Learn Go
intermediate1 min read

Error Handling

Go treats errors as ordinary values. A function that can fail returns an error as its last return value. The caller checks the error and decides what to do — there are no exceptions.

The error Interface

error is a built-in interface with a single method:

type error interface {
    Error() string
}

The standard way to return and check an error:

package main
 
import (
    "errors"
    "fmt"
)
 
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("divide by zero")
    }
    return a / b, nil
}
 
func main() {
    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    fmt.Printf("%.4f\n", result)
 
    _, err = divide(5, 0)
    if err != nil {
        fmt.Println("error:", err) // error: divide by zero
    }
}

Sentinel Errors

A sentinel error is a package-level variable used to signal a specific known condition:

package main
 
import (
    "errors"
    "fmt"
)
 
var ErrNotFound = errors.New("not found")
 
type Store struct {
    data map[string]string
}
 
func (s Store) Get(key string) (string, error) {
    v, ok := s.data[key]
    if !ok {
        return "", ErrNotFound
    }
    return v, nil
}
 
func main() {
    store := Store{data: map[string]string{"a": "apple"}}
 
    v, err := store.Get("a")
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(v) // apple
    }
 
    _, err = store.Get("z")
    if errors.Is(err, ErrNotFound) {
        fmt.Println("key not found — handle gracefully")
    }
}

Idiomatic Go: Export sentinel errors as var Err... and check them with errors.Is, not ==. Wrapping preserves the chain, and errors.Is traverses it.

Custom Error Types

Implement the error interface to attach structured data to an error:

package main
 
import (
    "errors"
    "fmt"
)
 
type ValidationError struct {
    Field   string
    Message string
}
 
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %q: %s", e.Field, e.Message)
}
 
func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{Field: "age", Message: "must be non-negative"}
    }
    if age > 150 {
        return &ValidationError{Field: "age", Message: "unrealistically large"}
    }
    return nil
}
 
func main() {
    err := validateAge(-5)
 
    var ve *ValidationError
    if errors.As(err, &ve) {
        fmt.Printf("Field: %s\nMessage: %s\n", ve.Field, ve.Message)
    }
}

Wrapping Errors with %w

Use fmt.Errorf with %w to wrap an error, preserving its identity while adding context:

package main
 
import (
    "errors"
    "fmt"
)
 
var ErrDatabase = errors.New("database error")
 
func fetchUser(id int) error {
    if id <= 0 {
        return fmt.Errorf("fetchUser: %w", ErrDatabase)
    }
    return nil
}
 
func getProfile(userID int) error {
    if err := fetchUser(userID); err != nil {
        return fmt.Errorf("getProfile: %w", err)
    }
    return nil
}
 
func main() {
    err := getProfile(-1)
    fmt.Println(err) // getProfile: fetchUser: database error
 
    fmt.Println(errors.Is(err, ErrDatabase)) // true — chain traversal
}

errors.Is vs errors.As

package main
 
import (
    "errors"
    "fmt"
)
 
type NotFoundError struct{ Name string }
 
func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%q not found", e.Name)
}
 
func main() {
    base := &NotFoundError{Name: "config.yaml"}
    wrapped := fmt.Errorf("load failed: %w", base)
 
    fmt.Println(errors.Is(wrapped, base)) // true
 
    var nfe *NotFoundError
    if errors.As(wrapped, &nfe) {
        fmt.Println("missing:", nfe.Name) // missing: config.yaml
    }
}

Key Takeaways