Skip to content

Wrapping 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 creates a new error whose message incorporates other values — but how it incorporates an error argument depends on the verb you choose:

  • %v formats the error’s message string and embeds it as plain text. The original error type is gone. The chain is cut.
  • %w records 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 severed
e1 := fmt.Errorf("dial: %v", base)
fmt.Println(errors.Is(e1, base)) // false
// %w — wraps, chain is preserved
e2 := fmt.Errorf("dial: %w", base)
fmt.Println(errors.Is(e2, base)) // true

Use %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 chain

This 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 *ValidationError
if 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.

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)
}
What is the difference between %v and %w in fmt.Errorf?
errors.Is(err, ErrNotFound) returns true when:
errors.As(err, &target) does what?
To make a custom error type unwrappable, you implement: