Error Wrapping

Course 3 · Ch 2
Error Wrapping: errors.Is, errors.As, and %w
Adding context to an error as it travels up through several function calls, without losing the ability to check what it originally was

Fundamentals Chapter 5 introduced error and nil. In real programs, an error often passes through several layers of function calls before reaching code that decides what to do about it — and each layer usually wants to add context ("failed while loading config" on top of "file not found"). Error wrapping is how Go adds that context without throwing away the original error underneath.

A Sentinel Error — A Known, Comparable Error Value

import "errors" var ErrNotFound = errors.New("item not found") func findItem(id int) (string, error) { if id != 1 { return "", ErrNotFound } return "Widget", nil }

A sentinel error — a specific, named error value declared once at package level — lets calling code check for that exact error later. This is the foundation everything else in this chapter builds on: a known error value that can be compared against.

Wrapping an Error with fmt.Errorf and %w

func loadItem(id int) (string, error) { name, err := findItem(id) if err != nil { return "", fmt.Errorf("loadItem: %w", err) } return name, nil } _, err := loadItem(5) fmt.Println(err) // loadItem: item not found

%w (used only with fmt.Errorf) wraps the original error inside a new one, attaching extra context ("loadItem: ") while keeping the original — ErrNotFound — retrievable from inside the new error. This differs from %v or %s, which would only produce a plain string with no way back to the original error value.

errors.Is — Checking for a Specific Error Through Any Number of Wraps

_, err := loadItem(5) if errors.Is(err, ErrNotFound) { fmt.Println("That item genuinely doesn't exist.") } else if err != nil { fmt.Println("Some other error occurred:", err) } // That item genuinely doesn't exist.

err != ErrNotFound (a direct comparison) would actually return true here, since err is now the wrapped version, not the original — direct comparison breaks once wrapping is involved. errors.Is(err, ErrNotFound) unwraps as many layers as necessary, checking whether ErrNotFound is anywhere inside the chain. This is the correct, wrap-aware way to ask "is this ultimately that specific error?"

Never compare a wrapped error directly with ==
Once an error has passed through fmt.Errorf("...: %w", err) even once, it is a brand-new error value — == against the original sentinel will always be false. errors.Is exists specifically to make this comparison correctly regardless of how many wrapping layers exist.

Custom Error Types

type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("%s: %s", e.Field, e.Message) } func validateAge(age int) error { if age < 0 { return &ValidationError{Field: "age", Message: "cannot be negative"} } return nil }

Any type with an Error() string method automatically satisfies Go's built-in error interface (Intermediate Chapter 2's structural typing again, applied to a single specific method). A custom error type like ValidationError can carry structured data — Field, Message — rather than just a flat message string.

errors.As — Extracting a Custom Error Type

err := validateAge(-5) var valErr *ValidationError if errors.As(err, &valErr) { fmt.Println("Invalid field:", valErr.Field) } // Invalid field: age

errors.As checks whether an error (possibly wrapped) matches a specific custom error type, and if so, assigns it into valErr so its specific fields (Field, Message) become accessible — something a plain error interface value alone never allows. errors.Is answers "is this that exact value?"; errors.As answers "is this that type, and if so, give it back to me properly typed."

ToolQuestion it answers
fmt.Errorf("...: %w", err)Add context while preserving the original error
errors.Is(err, sentinel)"Is this ultimately THIS specific error value?"
errors.As(err, &target)"Is this (or something it wraps) of THIS type? Give it to me."
err == sentinelUnsafe once wrapping is involved — avoid

Coding Challenges

Challenge 1

Declare var ErrEmptyName = errors.New("name cannot be empty"). Write a function greet(name string) error that returns this sentinel (wrapped with fmt.Errorf and extra context "greet: %w") if name is "". Use errors.Is to check the result and print an appropriate message.

📄 View solution
Challenge 2

Define a custom error type RangeError with fields Min and Max ints, and an Error() string method describing the valid range. Write a function checkAge(age int) error returning a *RangeError if age is outside 0-120. Use errors.As to extract it and print Min/Max.

📄 View solution
Challenge 3

Write three functions that call each other: layerOne calls layerTwo, layerTwo calls layerThree, and layerThree returns a sentinel error ErrFailed. Each layer wraps the error with its own name using fmt.Errorf("...: %w", err). Print the final error from layerOne, then confirm errors.Is(err, ErrFailed) is still true despite 3 layers of wrapping.

📄 View solution

Chapter 2 Quick Reference

  • var ErrX = errors.New("...") — a sentinel error, a known comparable value
  • fmt.Errorf("context: %w", err) — wraps err, adding context, keeping it unwrappable
  • errors.Is(err, sentinel) — true if sentinel appears anywhere in err's wrap chain
  • errors.As(err, &target) — extracts a specific custom error TYPE from the chain
  • type X struct {} + func (e *X) Error() string — defines a custom error type
  • Never use == on a wrapped error — it will not match, even when logically "the same"
  • Next chapter: context.Context — cancellation, timeouts, and request-scoped values