Error Handling
The error Interface
In Go, errors are values. The built-in error type is a simple interface:
Any type that implements Error() string satisfies the error interface. This means errors are ordinary values that can be passed around, stored, compared, and wrapped โ just like any other value in Go.
Functions that can fail return an error as their last return value. The caller is responsible for checking it:
Creating Simple Errors: errors.New
errors.New creates a static error with a fixed message:
Use errors.New when you need a simple, fixed message and don't need to add dynamic context.
Wrapping Errors: fmt.Errorf and %w
When an error propagates up through multiple layers, you should add context to explain what operation failed. fmt.Errorf with the %w verb wraps the original error while adding a message:
The %w verb (not %v or %e) is the key: it stores the original error inside the new one, allowing the error chain to be inspected later.
Sentinel Errors
Sentinel errors are package-level error values that callers can check against by identity. They represent expected, named failure conditions:
Define sentinel errors at the package level with a var declaration. The Err prefix is the conventional naming style.
errors.Is: Walking the Error Chain
errors.Is(err, target) checks whether target appears anywhere in the error chain โ it unwraps wrapped errors automatically. This makes it safe to use with wrapped errors:
Use errors.Is when you want to check for a specific sentinel error, regardless of how many layers of wrapping exist.
Custom Error Types
When you need to attach structured data to an error (such as HTTP status codes, field names, or operation names), define a custom struct that implements the error interface:
errors.As: Extracting a Typed Error
errors.As(err, &target) walks the error chain looking for an error that can be assigned to target. If found, it sets target and returns true. This lets you extract a typed error even when it has been wrapped:
The difference between errors.Is and errors.As:
errors.Ischecks identity โ is this specific error value in the chain?errors.Aschecks type โ is there an error of this type in the chain, and can I get it?
Idiomatic Error Handling Patterns
Always check errors immediately. The Go convention is to handle the error before proceeding:
Never silently discard errors. Assigning an error to _ hides bugs:
Add context when wrapping. The format "operation: %w" builds a readable error chain that helps with debugging.
Panic vs Error
panic is for programmer mistakes that should never happen in correct code โ not for expected runtime failures. Use errors for anything that could reasonably fail:
| Situation | Use |
|---|---|
| File not found, network timeout, invalid user input | error return |
| Index out of bounds in a function you control | panic |
| Nil pointer dereference you want to make explicit | panic |
| Initialisation that cannot possibly fail at runtime | panic (e.g., regexp.MustCompile) |
A function that accepts user input or calls external services must never panic โ always return an error. Panics are reserved for bugs in your own code that indicate the program is in an impossible state.
Knowledge Check
What verb wraps an error so errors.Is can unwrap it?
What is the difference between errors.Is and errors.As?
When should you use panic instead of returning an error?