Go treats errors as values, not as control flow exceptions. The built-in error interface has a single method — Error() string — and any type that implements it is an error. Functions return errors as their last return value, and callers check them explicitly with if err != nil. This design keeps error handling visible in the code, forces you to decide what to do at every call site, and makes the happy path and failure path equally readable.
Since Go 1.13, the errors and fmt packages support error wrapping. fmt.Errorf("opening config: %w", err) wraps an error with context while preserving the original for inspection. errors.Is checks if any error in the chain matches a target value, and errors.As extracts a specific error type from the chain. Together, these replace the fragile string-matching and type-assertion patterns that plagued earlier Go code.
The stdlib errors package is deliberately minimal: errors.New, errors.Is, errors.As, errors.Unwrap, and errors.Join (Go 1.20+). For richer error handling — stack traces, structured fields, error groups — the ecosystem provides packages like hashicorp/go-multierror and cockroachdb/errors. But the standard patterns cover the vast majority of production use cases.
Master go error handling
Take the Go Programming course with hands-on lessons and challenges.
Sentinel errors are package-level variables created with errors.New. They represent specific, well-known failure conditions that callers can check with errors.Is. The naming convention is ErrXxx.
Each function wraps the error with its own context using %w. The error message reads like a stack trace from outermost to innermost. Crucially, errors.Is can still find os.ErrNotExist at the bottom of the chain because %w preserves the unwrap chain.
errors.As extracts a specific error type from anywhere in the wrapping chain. This is the idiomatic way to inspect structured error data — fields like Field, Code, or StatusCode — without breaking the error chain or using fragile type assertions.
errors.Join merges multiple errors into one. The joined error implements Unwrap() []error, so errors.Is and errors.As traverse all branches. This eliminates the need for third-party multi-error libraries in most validation scenarios.
Implementing the Unwrap() error method on a custom error type plugs it into the standard error chain. errors.Is and errors.As will walk through RequestError to reach the inner fs.PathError and ultimately fs.ErrNotExist. This is how the entire stdlib works under the hood.
This shows the standard Go pattern: check errors immediately, wrap with context, return early. The happy path flows straight down with no nesting. Each error message includes the function name and the operation that failed, giving callers a clear trace from top to bottom.
Using `%v` instead of `%w` in fmt.Errorf, which destroys the error chain
Always use `%w` when you want callers to inspect the wrapped error with errors.Is or errors.As. Use `%v` only when you intentionally want to hide the underlying error type from callers — this is called "opaque wrapping" and is a deliberate API decision, not a default.
Comparing errors with `==` instead of errors.Is — breaks when errors are wrapped
Use `errors.Is(err, target)` instead of `err == target`. errors.Is walks the entire Unwrap chain, so it works regardless of how many layers of wrapping exist. Direct `==` only matches the outermost error.
Using type assertions `err.(*MyError)` instead of errors.As — breaks when errors are wrapped
Use `errors.As(err, &target)` instead of `err.(*MyError)`. Like errors.Is, errors.As traverses the chain. Type assertions only check the outermost type and will fail if any function along the way wrapped the error with fmt.Errorf.
Ignoring errors with `_ = someFunction()` or not checking the error return at all
Always handle errors explicitly. If you genuinely don't need the error (rare), add a comment explaining why. Use linters like `errcheck` or `golangci-lint` to catch silently discarded errors in CI.
Go error handling is built on a simple interface, explicit return values, and a wrapping/unwrapping chain. Functions return errors as their last value, callers check with if err != nil, and context flows through fmt.Errorf with %w. errors.Is matches sentinel values anywhere in the chain, errors.As extracts typed errors, and errors.Join combines multiple failures. The result is error handling that is verbose but predictable, composable, and easy to follow through any codebase.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.