Wrapping and Inspecting Errors
Overview
Go 1.13 introduced error wrapping, allowing you to add context while preserving the original error for inspection.
Wrapping Errors
originalErr := errors.New("file not found")
// %w wraps the error
wrappedErr := fmt.Errorf("loading config: %w", originalErr)
// The error chain: wrappedErr -> originalErrUnwrapping
err := fmt.Errorf("outer: %w",
fmt.Errorf("middle: %w",
errors.New("inner")))
inner := errors.Unwrap(err) // middle: innererrors.Is
Check if any error in the chain matches:
var ErrNotFound = errors.New("not found")
err := fmt.Errorf("user lookup: %w", ErrNotFound)
if errors.Is(err, ErrNotFound) {
// Handle not found
}errors.As
Extract specific error types:
type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string {
return "validation: " + e.Field
}
err := fmt.Errorf("processing: %w", &ValidationError{Field: "email"})
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Println("Invalid field:", valErr.Field)
}Custom Wrappers
type WrappedError struct {
Context string
Err error
}
func (e *WrappedError) Error() string {
return fmt.Sprintf("%s: %v", e.Context, e.Err)
}
func (e *WrappedError) Unwrap() error {
return e.Err
}Best Practices
// Add context when crossing boundaries
func LoadUser(id int) (*User, error) {
data, err := db.Query(id)
if err != nil {
return nil, fmt.Errorf("LoadUser(%d): %w", id, err)
}
return parse(data)
}Summary
| Function | Purpose |
|---|---|
fmt.Errorf("%w", err) |
Wrap error |
errors.Unwrap(err) |
Get wrapped error |
errors.Is(err, target) |
Check chain for match |
errors.As(err, &target) |
Extract typed error |
Worked example
Deep wrap chain with Is, As, and manual Unwrap walking.
Save as main.go. Then:
go mod init example
go run .package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
type HTTPError struct {
Code int
Err error
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("http %d: %v", e.Code, e.Err)
}
func (e *HTTPError) Unwrap() error { return e.Err }
func storeGet(key string) error {
if key == "" {
return ErrNotFound
}
return nil
}
func serviceGet(key string) error {
if err := storeGet(key); err != nil {
return fmt.Errorf("service.get(%q): %w", key, err)
}
return nil
}
func handlerGet(key string) error {
if err := serviceGet(key); err != nil {
return &HTTPError{Code: 404, Err: err}
}
return nil
}
func main() {
err := handlerGet("")
fmt.Println("chain:", err)
fmt.Println("Is NotFound:", errors.Is(err, ErrNotFound))
var he *HTTPError
if errors.As(err, &he) {
fmt.Println("As HTTPError code:", he.Code)
}
fmt.Print("unwrap walk:")
for e := err; e != nil; e = errors.Unwrap(e) {
fmt.Printf(" -> %v", e)
}
fmt.Println()
}Expected output:
chain: http 404: service.get(""): not found
Is NotFound: true
As HTTPError code: 404
unwrap walk: -> http 404: service.get(""): not found -> service.get(""): not found -> not found
More examples
errors.Join + Is: each joined error stays inspectable.
package main
import (
"errors"
"fmt"
)
var (
ErrName = errors.New("missing name")
ErrAge = errors.New("invalid age")
)
func validate(name string, age int) error {
var errs error
if name == "" {
errs = errors.Join(errs, ErrName)
}
if age < 0 {
errs = errors.Join(errs, ErrAge)
}
return errs
}
func main() {
err := fmt.Errorf("signup: %w", validate("", -3))
fmt.Println(err)
fmt.Println("Is name:", errors.Is(err, ErrName))
fmt.Println("Is age:", errors.Is(err, ErrAge))
fmt.Println("Is notfound:", errors.Is(err, errors.New("not found")))
}Expected output:
signup: missing name
invalid age
Is name: true
Is age: true
Is notfound: false
Runnable example
Save as main.go. Then:
go mod init example
go run .package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string {
return "validation: " + e.Field
}
type WrappedError struct {
Context string
Err error
}
func (e *WrappedError) Error() string {
return fmt.Sprintf("%s: %v", e.Context, e.Err)
}
func (e *WrappedError) Unwrap() error { return e.Err }
func loadUser(id int) error {
if id == 0 {
return fmt.Errorf("user lookup: %w", ErrNotFound)
}
if id < 0 {
return fmt.Errorf("processing: %w", &ValidationError{Field: "id"})
}
return nil
}
func main() {
err := loadUser(0)
fmt.Println("wrapped:", err)
fmt.Println("Is NotFound:", errors.Is(err, ErrNotFound))
fmt.Println("Unwrap once:", errors.Unwrap(err))
err = loadUser(-1)
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("As ValidationError field:", ve.Field)
}
// Custom Unwrap still participates in Is/As chains.
chain := &WrappedError{
Context: "handler",
Err: fmt.Errorf("service: %w", ErrNotFound),
}
fmt.Println("custom wrap:", chain)
fmt.Println("Is through custom wrap:", errors.Is(chain, ErrNotFound))
}Expected output:
wrapped: user lookup: not found
Is NotFound: true
Unwrap once: not found
As ValidationError field: id
custom wrap: handler: service: not found
Is through custom wrap: true
What to notice: %w builds a chain; errors.Is walks that chain for sentinel equality, errors.As extracts a concrete type. Implementing Unwrap() error opts your type into the same machinery.
Try next: Nest three layers of fmt.Errorf("%w") and walk with a loop on errors.Unwrap; try errors.Join of two validation failures and inspect with errors.Is.