Go

Go Error Handling👨‍💻

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.

Key Takeaways

  • 1The `error` interface is a single-method interface (`Error() string`) — any type can satisfy it, making custom errors trivially easy to create
  • 2Go returns errors as values rather than throwing exceptions, which keeps error handling explicit and visible at every call site
  • 3Error wrapping with `fmt.Errorf("context: %w", err)` adds context while preserving the original error chain for inspection
  • 4`errors.Is(err, target)` walks the entire error chain checking for a match by value — use it for sentinel errors like `io.EOF` or `sql.ErrNoRows`
  • 5`errors.As(err, &target)` walks the chain checking for a match by type and extracts the first matching error — use it for custom error types with extra fields
  • 6`errors.Join` (Go 1.20+) combines multiple errors into a single error that `errors.Is` and `errors.As` can unwrap, replacing third-party multi-error packages

Master go error handling

Take the Go Programming course with hands-on lessons and challenges.

Examples

The error interface and errors.New

go

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.

Error wrapping with fmt.Errorf and %w

go

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.

Custom error types with errors.As

go

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 for combining multiple errors (Go 1.20+)

go

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.

Custom error type implementing Unwrap for error chains

go

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.

Idiomatic error handling in a real function

go

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.

Common Mistakes

Mistake:

Using `%v` instead of `%w` in fmt.Errorf, which destroys the error chain

Fix:

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.

Mistake:

Comparing errors with `==` instead of errors.Is — breaks when errors are wrapped

Fix:

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.

Mistake:

Using type assertions `err.(*MyError)` instead of errors.As — breaks when errors are wrapped

Fix:

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.

Mistake:

Ignoring errors with `_ = someFunction()` or not checking the error return at all

Fix:

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.

Best Practices

  • Wrap errors with context at every layer using fmt.Errorf("doing X: %w", err) — the resulting message should read like a breadcrumb trail from high-level operation to root cause
  • Define sentinel errors (var ErrNotFound = errors.New(...)) for conditions that callers need to programmatically handle, and document them as part of your package's API
  • Keep error messages lowercase and without trailing punctuation — they get concatenated in chains, so "loadConfig: parse JSON: unexpected comma" reads naturally
  • Return errors rather than logging and continuing — let the caller at the top of the call stack decide whether to log, retry, or exit
  • Use errors.Join for validation functions that collect multiple independent failures, avoiding the need for third-party multi-error packages

Summary

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.

Practice Go with hands-on challenges

Learn go error handling hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Go with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.