Skip to content

Wrap errors with %w and context; never log and return

Wrapped errors keep the cause inspectable and say where it failed, and one owner handles each error.

ID go-errors-wrap
Strength must (error)
Status accepted
Scope go
Applies to **/*.go
Tags errors, reliability, debugging
Updated 2026-10-27

Context

An error that crosses a package boundary without context ("open: no such file") forces whoever reads the log to guess which call failed. Logging an error and then returning it means the same failure shows up two or three times, at different levels, with no clear owner.

Decision

Return errors wrapped with fmt.Errorf("doing x: %w", err) when they cross a package or a meaningful step. Compare with errors.Is and errors.As, never with == or string matching. An error is either handled (logged, retried, turned into a response) or returned, never both.

Consequences

Error messages read as a chain of steps from the top caller down to the cause. wrapcheck and errorlint enforce the mechanics; reviewers still check that the added context says something useful.

Exceptions

Sentinel errors returned unchanged from the package that defines them.

Examples

// good
if err := os.WriteFile(path, data, 0o600); err != nil {
    return fmt.Errorf("write config %s: %w", path, err)
}

// bad: logged and returned, and the cause is flattened to a string
if err != nil {
    log.Printf("write failed: %v", err)
    return fmt.Errorf("write failed: %v", err)
}

Enforced by

Rule Checker What it checks
golangci-errors golangci-lint golangci-lint with wrapcheck and errorlint enabled

Read it from an agent with canon explain go-errors-wrap, or the canon://records/go-errors-wrap resource.