Wrapping Errors
Why wrap errors?
Section titled “Why wrap errors?”When a function calls another and it fails, returning the raw error loses context. The caller sees “database error” but has no idea which operation triggered it, which request it belonged to, or at what layer it occurred. Wrapping adds a layer of context — what were you trying to do? — while preserving the original error so it can still be inspected.
The idiomatic pattern in Go:
if err != nil { return fmt.Errorf("description: %w", err)}The %w verb is the key. It wraps the original error inside the new one, keeping the full chain intact for callers who need to inspect it.
fmt.Errorf and %w
Section titled “fmt.Errorf and %w”fmt.Errorf creates a new error whose message incorporates other values — but how it incorporates an error argument depends on the verb you choose:
%vformats the error’s message string and embeds it as plain text. The original error type is gone. The chain is cut.%wrecords the original error by value inside the new error. The message still contains the original message, but the original is also accessible programmatically.
base := errors.New("connection refused")
// %v — message only, chain is severede1 := fmt.Errorf("dial: %v", base)fmt.Println(errors.Is(e1, base)) // false
// %w — wraps, chain is preservede2 := fmt.Errorf("dial: %w", base)fmt.Println(errors.Is(e2, base)) // trueUse %w when you want callers upstream to be able to identify or extract the original error. Use %v only when you intentionally want to discard the error type — for example, when logging and the chain is irrelevant.
errors.Is — checking identity through a chain
Section titled “errors.Is — checking identity through a chain”errors.Is(err, target) walks the entire unwrap chain of err and returns true if any error in the chain matches target. Matching is done by identity (==) by default, not by message string.
var ErrNotFound = errors.New("not found")
err := fmt.Errorf("getUser: %w", fmt.Errorf("queryDB: %w", ErrNotFound))
fmt.Println(errors.Is(err, ErrNotFound)) // true — found deep in the chainThis works regardless of how deeply the target is nested. You define a sentinel error once (a package-level var) and check for it anywhere in the call stack.
errors.As — extracting a type from the chain
Section titled “errors.As — extracting a type from the chain”errors.As(err, &target) also walks the chain, but instead of checking identity, it checks type. If any error in the chain can be assigned to target’s type, it performs the assignment and returns true.
var ve *ValidationErrorif errors.As(err, &ve) { fmt.Println(ve.Field) // access fields on the concrete type}Use errors.As when you have a custom error type that carries extra information — field names, HTTP status codes, retry hints — and you need to access those fields, not just check that the error exists.
Custom error types
Section titled “Custom error types”Any struct that implements Error() string satisfies the error interface. Custom types let you carry structured context alongside the message:
type ValidationError struct { Field string Message string}
func (e *ValidationError) Error() string { return fmt.Sprintf("validation: field %q %s", e.Field, e.Message)}To make a custom error type part of a wrapping chain, implement Unwrap() error:
type AppError struct { Code int Err error}
func (e *AppError) Error() string { return fmt.Sprintf("code %d: %v", e.Code, e.Err) }func (e *AppError) Unwrap() error { return e.Err }With Unwrap in place, errors.Is and errors.As can see through AppError to reach whatever it wraps.
package main
import ( "errors" "fmt")
var ErrDatabase = errors.New("database error")
type ValidationError struct { Field string Message string}
func (e *ValidationError) Error() string { return fmt.Sprintf("validation: field %q %s", e.Field, e.Message)}
func queryDB(id int) (string, error) { if id < 0 { return "", ErrDatabase } if id == 0 { return "", &ValidationError{Field: "id", Message: "must be positive"} } return "Alice", nil}
func getUser(id int) (string, error) { name, err := queryDB(id) if err != nil { return "", fmt.Errorf("getUser %d: %w", id, err) } return name, nil}
func main() { _, err := getUser(-1) if err != nil { fmt.Println(err) fmt.Println("is ErrDatabase:", errors.Is(err, ErrDatabase)) }
_, err = getUser(0) if err != nil { fmt.Println(err) var ve *ValidationError if errors.As(err, &ve) { fmt.Printf("field=%q msg=%q\n", ve.Field, ve.Message) } }
name, err := getUser(1) if err != nil { fmt.Println("error:", err) return } fmt.Println("name:", name)}Loading Go runtime (first run only, ~8 MB)…