intermediate
Step 8 of 15
Error Handling
Go Programming
Error Handling
Go takes a unique approach to error handling that differs from most other languages. Instead of exceptions and try/catch blocks, Go uses explicit error return values. Functions that can fail return an error as their last return value, and the caller is expected to check it immediately. This approach makes error handling visible and explicit in the code flow, preventing errors from being silently ignored. While it can lead to verbose code, it makes error handling paths clear and easy to follow.
Error Handling Patterns
package main
import (
"errors"
"fmt"
"os"
"strconv"
)
// Functions return errors as the last value
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
// Always check errors immediately
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
// Error wrapping (Go 1.13+)
data, err := os.ReadFile("config.json")
if err != nil {
wrapped := fmt.Errorf("failed to load config: %w", err)
fmt.Println(wrapped)
// "failed to load config: open config.json: no such file or directory"
// Unwrap to check the original error
if errors.Is(wrapped, os.ErrNotExist) {
fmt.Println("File does not exist")
}
}
_ = data
// Converting strings to numbers
num, err := strconv.Atoi("not-a-number")
if err != nil {
fmt.Println("Conversion error:", err)
}
_ = num
}
Custom Error Types
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
type NotFoundError struct {
Resource string
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s with ID %d not found", e.Resource, e.ID)
}
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, &ValidationError{Field: "id", Message: "must be positive"}
}
// Simulate not found
return nil, &NotFoundError{Resource: "User", ID: id}
}
// Check error type with errors.As
user, err := findUser(999)
if err != nil {
var notFound *NotFoundError
if errors.As(err, ¬Found) {
fmt.Printf("Not found: %s #%d\n", notFound.Resource, notFound.ID)
}
var validErr *ValidationError
if errors.As(err, &validErr) {
fmt.Printf("Validation: %s - %s\n", validErr.Field, validErr.Message)
}
}
// Sentinel errors (predefined error values)
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
)
func getResource(id int, role string) (string, error) {
if role != "admin" {
return "", fmt.Errorf("access denied: %w", ErrForbidden)
}
return "resource data", nil
}
Pro tip: Usefmt.Errorf("context: %w", err)to wrap errors with additional context while preserving the original error for inspection witherrors.Is()anderrors.As(). This creates a chain of errors that shows exactly where things went wrong: "save user: validate: email: invalid format".
Key Takeaways
- Go uses explicit error return values instead of exceptions — always check
if err != nil. - Wrap errors with
fmt.Errorf("context: %w", err)to add context while preserving the original error. - Use
errors.Is()to compare against sentinel errors anderrors.As()to extract custom error types. - Create custom error types by implementing the
errorinterface (Error() stringmethod). - Use
defer,panic, andrecoveronly for truly exceptional situations, not normal error handling.