Go errors Package

Create errors, wrap them with context, and inspect chains with errors.Is, errors.As, and custom error types.

errors.New fmt.Errorf %w errors.Is errors.As errors.Unwrap errors.Join Custom types
๐Ÿ†•

Creating Errors

โ„น๏ธ
Go errors are just values that implement the error interface: Error() string. Use errors.New for static messages and fmt.Errorf when you need to embed dynamic values.
errors.New โ€” simple static errors errors.New
import "errors"

// Define sentinel errors at package level
var (
    ErrNotFound   = errors.New("not found")
    ErrPermission = errors.New("permission denied")
    ErrTimeout    = errors.New("operation timed out")
)

func findUser(id int) (*User, error) {
    if id <= 0 {
        return nil, ErrNotFound
    }
    // ...
}
fmt.Errorf โ€” errors with context fmt.Errorf
import "fmt"

func loadConfig(path string) error {
    _, err := os.Open(path)
    if err != nil {
        // %w wraps the original error
        return fmt.Errorf("loadConfig %q: %w", path, err)
    }
    return nil
}

// Without %w โ€” context only, no unwrapping
return fmt.Errorf("invalid port %d", port)
๐ŸŽ

Wrapping & Unwrapping

๐Ÿ’ก
Wrap errors as they travel up the call stack to add context at each layer. The convention is "operationName context: %w" โ€” a colon-separated chain of what went wrong and where.
Building an error chain with %w Wrapping
// Low-level: returns a raw OS error
func readFile(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("readFile: %w", err)
    }
    return data, nil
}

// Mid-level: adds more context
func loadConfig(env string) (*Config, error) {
    data, err := readFile("/etc/app/" + env + ".json")
    if err != nil {
        return nil, fmt.Errorf("loadConfig %q: %w", env, err)
    }
    // ...
}

// Resulting error message:
// loadConfig "prod": readFile: open /etc/app/prod.json: no such file or directory
errors.Unwrap โ€” peel one layer off Unwrap
var ErrBase = errors.New("base error")
wrapped := fmt.Errorf("layer 1: %w", ErrBase)
wrapped2 := fmt.Errorf("layer 2: %w", wrapped)

errors.Unwrap(wrapped2) // layer 1: base error
errors.Unwrap(wrapped)  // base error (ErrBase)
errors.Unwrap(ErrBase)  // nil โ€” nothing left to unwrap

// In practice you rarely call Unwrap directly.
// Use errors.Is / errors.As instead โ€” they traverse the chain for you.
๐Ÿ”

errors.Is

โ„น๏ธ
errors.Is(err, target) walks the entire error chain (unwrapping at each step) and returns true if any error in the chain equals target. Never use == for wrapped errors.
errors.Is โ€” check for a specific sentinel error errors.Is
import (
    "database/sql"
    "errors"
)

func getUser(id int) (*User, error) {
    row, err := db.QueryRow("SELECT ...", id)
    if err != nil {
        return nil, fmt.Errorf("getUser %d: %w", id, err)
    }
    return row, nil
}

user, err := getUser(99)
if errors.Is(err, sql.ErrNoRows) {
    // Correctly matches even though err is wrapped
    fmt.Println("user not found")
}

// DO NOT use == for wrapped errors
if err == sql.ErrNoRows { // WRONG โ€” misses wrapped errors }
Custom Is method โ€” value-based matching Custom Is
// Implement Is on your type when equality needs custom logic
type HTTPError struct {
    Code    int
    Message string
}

func (e *HTTPError) Error() string {
    return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message)
}

// Is matches on Code alone, ignoring Message
func (e *HTTPError) Is(target error) bool {
    t, ok := target.(*HTTPError)
    return ok && e.Code == t.Code
}

ErrNotFound := &HTTPError{Code: 404}

err := fmt.Errorf("request failed: %w", &HTTPError{Code: 404, Message: "page gone"})
errors.Is(err, ErrNotFound) // true โ€” Code 404 matches
๐ŸŽฏ

errors.As

๐Ÿ’ก
errors.As(err, &target) walks the chain and assigns the first error that can be assigned to target's type. Use it when you need to read fields off a specific error type.
errors.As โ€” extract a typed error from the chain errors.As
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation: %s %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{Field: "age", Message: "must be non-negative"}
    }
    return nil
}

err := fmt.Errorf("createUser: %w", validateAge(-1))

var ve *ValidationError
if errors.As(err, &ve) {
    // ve is now populated with the ValidationError
    fmt.Printf("bad field: %s โ€” %s\n", ve.Field, ve.Message)
    // bad field: age โ€” must be non-negative
}
Custom As method โ€” control how As matches your type Custom As
// Implement As when your error wraps another and you need
// errors.As to "see through" into a nested error field
type DBError struct {
    Query string
    Cause error
}

func (e *DBError) Error() string  { return e.Cause.Error() }
func (e *DBError) Unwrap() error { return e.Cause }

// errors.As walks Unwrap() automatically โ€” no custom As needed here.
// Only implement As when Unwrap isn't sufficient.
๐Ÿ—๏ธ

Custom Error Types

โ„น๏ธ
Any type that implements Error() string satisfies the error interface. Add an Unwrap() error method to make your type work seamlessly with errors.Is and errors.As.
Structured error type with Unwrap Custom Type
type RequestError struct {
    StatusCode int
    URL        string
    Err        error
}

func (e *RequestError) Error() string {
    return fmt.Sprintf("%s โ†’ HTTP %d: %v", e.URL, e.StatusCode, e.Err)
}

// Unwrap lets errors.Is and errors.As look inside
func (e *RequestError) Unwrap() error { return e.Err }

// Usage
err := &RequestError{
    StatusCode: 503,
    URL:        "https://api.example.com/data",
    Err:        ErrServiceUnavailable,
}

var re *RequestError
if errors.As(err, &re) {
    fmt.Printf("failed URL: %s (status %d)\n", re.URL, re.StatusCode)
}
Sentinel errors vs. typed errors Pattern
// Sentinel: simple, check with errors.Is
var ErrNotFound = errors.New("not found")

// Typed: carries data, check with errors.As
type NotFoundError struct{ Resource string; ID int }
func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s %d not found", e.Resource, e.ID)
}

// Caller pattern
var nfe *NotFoundError
switch {
case errors.As(err, &nfe):
    fmt.Printf("missing %s #%d\n", nfe.Resource, nfe.ID)
case errors.Is(err, ErrNotFound):
    fmt.Println("something not found")
default:
    fmt.Println("unexpected error:", err)
}
๐Ÿ”—

errors.Join

โ„น๏ธ
errors.Join (Go 1.20) combines multiple errors into one. The result's Unwrap returns a slice, so errors.Is and errors.As check all of them.
errors.Join โ€” accumulate multiple errors Go 1.20+
func validateUser(u User) error {
    var errs []error
    if u.Name == "" {
        errs = append(errs, errors.New("name is required"))
    }
    if u.Age < 0 {
        errs = append(errs, errors.New("age must be non-negative"))
    }
    if u.Email == "" {
        errs = append(errs, errors.New("email is required"))
    }
    return errors.Join(errs...)  // nil if errs is empty
}

err := validateUser(User{Age: -1})
// err.Error():
// name is required
// age must be non-negative
// email is required

// errors.Is still works across the joined set
errors.Is(err, ErrSomeSentinel) // checks all joined errors
๐Ÿ“

Patterns & Best Practices

Idiomatic error handling flow Pattern
// Return errors immediately โ€” keep the happy path at the left margin
func process(id int) (string, error) {
    user, err := getUser(id)
    if err != nil {
        return "", fmt.Errorf("process: %w", err)
    }

    data, err := fetchData(user.Token)
    if err != nil {
        return "", fmt.Errorf("process fetchData: %w", err)
    }

    return transform(data), nil
}

// Handle errors at the right level โ€” wrap when passing up,
// handle (log/respond) when you can do something about it
result, err := process(42)
if err != nil {
    var nfe *NotFoundError
    if errors.As(err, &nfe) {
        http.Error(w, nfe.Error(), http.StatusNotFound)
        return
    }
    http.Error(w, "internal error", http.StatusInternalServerError)
    log.Printf("process: %v", err)
    return
}
Do: wrap with context Do
// Add what, where, and relevant IDs
return fmt.Errorf("getOrder %d: %w", id, err)

// One wrap per function boundary is enough
func Save(u *User) error {
    if err := db.Insert(u); err != nil {
        return fmt.Errorf("Save: %w", err)
    }
    return nil
}
Don't: log and return Don't
// Never log AND return โ€” caller logs too โ†’ duplicate noise
if err != nil {
    log.Printf("error: %v", err) // BAD
    return err
}

// Never swallow errors silently
result, _ := riskyOp() // BAD โ€” ignoring errors

// Either handle it or return it, not both
๐Ÿ“‹

Quick Reference

Function / Method Returns Description
errors.New(text)errorCreate a simple error with a static message
fmt.Errorf(format, ...)errorCreate error with formatted message; use %w to wrap
errors.Is(err, target)boolWalk chain; true if any error equals target
errors.As(err, &target)boolWalk chain; assign first matching typed error
errors.Unwrap(err)errorReturn the directly wrapped error, or nil
errors.Join(errs...)errorCombine multiple errors; nil if all nil (Go 1.20+)
Error() stringstringInterface method โ€” implement to create custom errors
Unwrap() errorerrorImplement on custom types to enable chain traversal
Is(error) boolboolImplement for custom equality logic in errors.Is
As(any) boolboolImplement for custom matching logic in errors.As