Go Error Handling Cheat Sheet
Covers Go's explicit error return values, wrapping errors with %w, errors.Is and errors.As, custom error types, and panic/recover semantics.
Basic Error Handling
The idiomatic (result, error) return pattern.
func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") // Create a simple error } return a / b, nil}result, err := divide(10, 0)if err != nil { fmt.Println("Error:", err) return}fmt.Println("Result:", result)
Wrapping & Unwrapping Errors
Adding context to errors while preserving the original.
var ErrNotFound = errors.New("not found")func findUser(id int) error { if id < 0 { return fmt.Errorf("findUser: invalid id %d: %w", id, ErrNotFound) // %w wraps the error } return nil}err := findUser(-1)if errors.Is(err, ErrNotFound) { // Checks the wrapped chain for a specific sentinel error fmt.Println("user not found")}var myErr *MyErrorif errors.As(err, &myErr) { // Extracts a specific error type from the chain fmt.Println(myErr.Code)}
Custom Errors & panic/recover
Defining error types and handling unrecoverable states.
type MyError struct { Code int Message string}func (e *MyError) Error() string { // Satisfies the error interface return fmt.Sprintf("[%d] %s", e.Code, e.Message)}func mustPositive(n int) { if n < 0 { panic("n must be positive") // Panic for unrecoverable programmer errors }}func safeCall() { defer func() { if r := recover(); r != nil { // Recover stops a panic from crashing the program fmt.Println("recovered:", r) } }() mustPositive(-1)}
Idioms
Conventions the Go community relies on.
- error interface- type error interface { Error() string }; any type with that method is an error
- Multiple returns- Go functions typically return (result, error); always check err before using the result
- errors.New / fmt.Errorf- Create simple errors or formatted errors; %w wraps an inner error
- errors.Is- Tests whether an error, or any error it wraps, matches a target sentinel error
- errors.As- Finds the first error in the chain matching a target type and assigns it
- panic/recover- Used for truly exceptional, unrecoverable situations, not routine error handling
- defer- Schedules a function call to run when the surrounding function returns (used for cleanup/recover)
Combining Multiple Errors with errors.Join
Go 1.20+ lets you aggregate independent failures into one error value.
func closeAll(files []*os.File) error { var errs []error for _, f := range files { if err := f.Close(); err != nil { errs = append(errs, fmt.Errorf("closing %s: %w", f.Name(), err)) } } return errors.Join(errs...) // nil if errs is empty, else a multi-error}err := closeAll(openFiles)if err != nil { fmt.Println(err) // each joined error printed on its own line}// errors.Is / errors.As still traverse every branch of a joined error tree
Sentinel Errors vs. Typed Errors
Choosing the right comparison strategy depending on how much context callers need.
// Sentinel: identity comparison, no extra datavar ErrPermissionDenied = errors.New("permission denied")// Typed: carries structured data callers can extracttype ValidationError struct { Field string Rule string}func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Rule)}func validate(age int) error { if age < 0 { return &ValidationError{Field: "age", Rule: "must be non-negative"} } return nil}var ve *ValidationErrorif errors.As(validate(-1), &ve) { fmt.Println("bad field:", ve.Field) // structured access, not just a string}
Wrapping Errors via Named Returns + defer
A single defer can add context to every error path in a function.
func processFile(path string) (err error) { defer func() { if err != nil { err = fmt.Errorf("processFile(%s): %w", path, err) } }() f, err := os.Open(path) if err != nil { return err // gets wrapped by the defer above } defer f.Close() if err := parse(f); err != nil { return err // also gets wrapped, without repeating context at every return } return nil}
context.Context Cancellation Errors
Distinguishing timeouts from explicit cancellation in concurrent code.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel()err := doRequest(ctx)switch {case errors.Is(err, context.DeadlineExceeded): fmt.Println("timed out")case errors.Is(err, context.Canceled): fmt.Println("caller canceled the operation")case err != nil: fmt.Println("other failure:", err)}// ctx.Err() returns exactly one of DeadlineExceeded, Canceled, or nil
Advanced Error Idioms
Patterns used in production-grade Go services beyond basic error checks.
- errors.Join- Aggregates multiple errors into one, tested individually via errors.Is/errors.As across the whole tree
- Unwrap() error / Unwrap() []error- Implement one of these methods to make a custom error type participate in wrapping chains
- Sentinel identity- Sentinel errors must be package-level vars compared by identity, never recreated with errors.New at call time
- Error message style- Lowercase, no trailing punctuation, no capitalization -- error strings are often composed into larger messages
- Don't log and return- Log an error at the point it's handled, not at every layer it passes through, to avoid duplicate noise
- panic in goroutines- A panic in a goroutine that isn't recovered crashes the entire process, even if the caller uses recover elsewhere
- errors.Unwrap- Manually walks one level of the wrap chain; errors.Is/As do this repeatedly and also check Is(error) bool methods
Reserve panic for programmer errors and unrecoverable states — for expected failure conditions like bad input, a missing file, or a network error, always return an error value instead.