go 86 lines · 3 tabs

Wrapping Errors with Context and Inspecting Them via errors.Is and errors.As in Go

Shared by codesnips Aug 2026
3 tabs
package store

import (
	"errors"
	"fmt"
	"time"
)

var ErrNotFound = errors.New("resource not found")

type RateLimitError struct {
	RetryAfter time.Duration
}

func (e *RateLimitError) Error() string {
	return fmt.Sprintf("rate limited, retry after %s", e.RetryAfter)
}
3 files · go Explain with highlit

This snippet shows the idiomatic Go pattern for adding context to errors as they bubble up a call stack while keeping them programmatically inspectable. The core idea is that fmt.Errorf with the %w verb wraps an underlying error rather than flattening it into an opaque string, so callers can still ask questions about the original cause using errors.Is and errors.As.

errors.go defines the building blocks. ErrNotFound is a sentinel value — a package-level error compared by identity — which suits conditions that carry no extra data. RateLimitError is a typed error: it implements the error interface and carries a RetryAfter field, so consumers need the concrete type to read that field. This is the key distinction: sentinels answer "is this that specific condition?" while typed errors answer "give me the structured details".

store.go is the layer that produces those errors. Fetch maps HTTP status codes onto the error vocabulary, wrapping each with %w so the request URL travels along as human context without hiding the machine-readable cause. A 404 wraps ErrNotFound; a 429 wraps a freshly built RateLimitError whose RetryAfter is parsed from the response header. Because both are wrapped, the string you log is descriptive and the chain stays intact.

consumer.go demonstrates inspection at the top of the stack. errors.Is(err, ErrNotFound) walks the wrap chain looking for the sentinel, matching regardless of how many %w layers sit on top. errors.As(err, &rlErr) searches the same chain for the first value assignable to a *RateLimitError, populating it so rlErr.RetryAfter becomes accessible.

The trade-off worth understanding: wrapping with %w makes an error part of your package's API, since callers may now depend on unwrapping it — use %v instead when the cause should stay private. A common pitfall is reaching for string matching (strings.Contains(err.Error(), ...)), which is brittle; errors.Is/errors.As are the correct tools. Reach for this pattern whenever an error must survive several layers yet still drive a decision, such as retry logic, at the boundary.


Related snips

Share this code

Here's the card — post it anywhere.

Wrapping Errors with Context and Inspecting Them via errors.Is and errors.As in Go — share card
Link copied