Error Wrapping with %w & errors.As
Wrapping adds context to failures while preserving the original cause for inspection.
Search across all documentation pages
Wrapping adds context to failures while preserving the original cause for inspection.
fmt.Errorf with %w builds an unwrap chain; errors.Is and errors.As traverse that chain so callers recognize sentinels and custom types through intermediate layers.
Go 1.13 standardized error wrapping: each layer adds operation context (read config, dial db) without destroying the root os.ErrNotExist or your domain type.
errors.As extracts typed data when a sentinel is too coarse.
Use %v when the inner error must not participate in Is/As.
Quick-reference recipe card - copy-paste ready.
if err != nil {
return fmt.Errorf("fetch user %q: %w", id, err)
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
log.Println("path:", pathErr.Path)
}
if errors.Is(err, os.ErrNotExist) {
return ErrNotFound
}When to reach for this:
errors.Joinpackage main
import (
"errors"
"fmt"
"net"
"os"
)
type OpError struct {
Op string
}
func (e OpError) Error() string {
return fmt.Sprintf("%s failed", e.Op)
}
func readDB(path string) error {
_, err := os.ReadFile(path)
if err != nil {
return OpError{Op: "read"}
}
return nil
}
func loadUser(path string) error {
if err := readDB(path); err != nil {
return fmt.Errorf("load user from %q: %w", path, err)
}
return nil
}
func main() {
err := loadUser("missing.db")
var op OpError
if errors.As(err, &op) {
fmt.Println("operation:", op.Op)
}
fmt.Println("not exist:", errors.Is(err, os.ErrNotExist))
fmt.Println("net timeout:", errors.Is(err, net.ErrClosed))
}What this demonstrates:
OpError is reachable through a fmt.Errorf wraperrors.As requires a pointer to the target typeerrors.Is still finds os.ErrNotExist under OpError if the chain includes itIs/As are for program logic%w creates a wrapper implementing Unwrap() error.errors.Unwrap(err) returns one level; Is/As loop until match or nil.%w per fmt.Errorf call is allowed.errors.Join(errs...) (Go 1.20+) returns an error that unwraps to multiple values; Is/As check each.| Verb | Unwrap chain | errors.Is / errors.As |
|---|---|---|
%w | Preserves inner | Works through wrap |
%v | No unwrap link | Inner not visible to Is/As |
error or is a pointer to a struct field.// Typed extraction
var pe *os.PathError
if errors.As(err, &pe) {
_ = pe.Path
}
// Manual one-level unwrap (rare)
inner := errors.Unwrap(err)%w in one Errorf - Compile error. Fix: Wrap once per call; chain with nested fmt.Errorf.var target MyErr; errors.As(err, &target).errors.As into *MyErr vs MyErr must match what you return. Fix: Be consistent; document constructor return type.errors.Is is simpler for var ErrX. Fix: Reserve As for types with fields.%w plus verbose logs can expose tokens. Fix: Sanitize messages; use %v at public boundaries.| Alternative | Use When | Don't Use When |
|---|---|---|
errors.Is only | Sentinel identity is enough | You need structured fields |
| Flat errors without wrap | Single-layer CLI tool | Multi-package services need cause chains |
status.Convert (gRPC) | RPC transport mapping | Pure domain layer |
Custom Unwrap []error | Multi-error aggregation | Simple single-failure paths |
It wraps the error argument so the result implements Unwrap and participates in errors.Is and errors.As.
errors.As walks the unwrap chain. A type assertion only inspects the top-level dynamic type.
fmt.Errorf("msg: %w", nil) produces an error with nil unwrap. Avoid wrapping; return nil on success paths.
No hard limit, but deep chains hint at missing boundary logging. Prefer context at meaningful layers.
Yes. Match the pointer shape you return (*MyErr) and pass &target of the correct type to As.
When several operations fail in parallel (validation, fan-in) and you want one returned error listing all causes.
Low-level access to the immediate inner error. Most code uses Is/As instead of manual loops.
Yes, add context at your API boundary (package x: operation failed). Do not log and wrap redundantly without new information.
Implement Unwrap() error on your type or use fmt.Errorf with %w for standard behavior.
Helpers like os.IsNotExist use errors.Is internally, so they work through %w chains containing os.ErrNotExist.
Use Is for sentinels mapping to status codes; As when extracting error details for structured JSON responses.
Small allocation cost per wrap. Clarity and debuggability usually dominate unless profiling shows a hot path issue.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026