Go Error Handling Patterns
Learn idiomatic Go error handling patterns including custom errors, wrapping, sentinel errors, and best practices.
Go's Error Philosophy
Go treats errors as values, not exceptions. Functions return errors as their last return value, and callers must explicitly handle them.
``go
file, err := os.Open("config.json")
if err != nil {
return fmt.Errorf("failed to open config: %w", err)
}
defer file.Close()
`
Creating Errors
Simple Errors
`go
import "errors"
// errors.New for static error messages var ErrNotFound = errors.New("resource not found")
// fmt.Errorf for formatted errors
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("invalid user ID: %d", id)
}
// ...
}
`
Custom Error Types
`go
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string { return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message) }
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Message: "must be between 0 and 150",
}
}
return nil
}
`
Error Wrapping (Go 1.13+)
`go
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("readConfig(%s): %w", path, err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
return &config, nil
}
`
Unwrapping with errors.Is and errors.As
`go
// errors.Is checks if an error matches a target
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File doesn't exist")
}
// errors.As checks if error can be cast to a type
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field %s: %s\n", valErr.Field, valErr.Message)
}
`
Sentinel Errors
`go
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
)
func getUser(id string) (*User, error) {
user, ok := users[id]
if !ok {
return nil, ErrNotFound
}
return user, nil
}
`
Best Practices
1. Handle errors immediately — don't ignore them
2. Add context when wrapping: fmt.Errorf("doing X: %w", err)
3. Use sentinel errors for expected conditions callers need to check
4. Use custom error types when callers need structured error data
5. Don't panic in library code — return errors instead
6. Log at the top level — not at every layer
`go
// Good: Handle or return, add context
func processOrder(orderID string) error {
order, err := getOrder(orderID)
if err != nil {
return fmt.Errorf("processOrder(%s): %w", orderID, err)
}
// process order...
return nil
}
// Bad: Ignoring errors file, _ := os.Open("data.txt") // Don't do this! ``