← Files GophersARCHIVED FILE

skills/go-error-handling/references/wrapping-vs-shadowing.md

2.84 KB · Oct 3, 2026 · 06:31 UTC

↓ Download file

# Wrapping (`%w`) vs Shadowing (`%v`)

`fmt.Errorf` supports both. The difference is whether the underlying error remains inspectable.

## `%w` — Expose

The wrapped error is reachable via `errors.Is` and `errors.As`.

```go
if err := db.Get(id); err != nil {
    return fmt.Errorf("loading user %d: %w", id, err)
}

// Caller can still do:
errors.Is(err, sql.ErrNoRows) // true
```

**Use `%w` by default.** Adding context (what you were doing, with which inputs) while preserving identity is almost always what you want.

## `%v` — Hide

The error becomes a plain string. `errors.Is`/`As` cannot reach into it.

```go
return fmt.Errorf("loading user %d: %v", id, err)
// errors.Is(err, sql.ErrNoRows) is false
```

**Only use `%v` when you intentionally want to hide an implementation detail** — typically because the underlying error type is unstable, internal, or would leak abstraction.

```go
// The user of mypkg should not depend on sqlx error types
return fmt.Errorf("save: %v", sqlxErr)
```

When you do this, add a comment explaining why. A reviewer should never have to guess.

## When to Wrap

Wrap when **all three** are true:

1. You are adding meaningful context (a noun + an input)
2. You are on a layer boundary (DB → repo, repo → service, service → handler)
3. The caller might reasonably need to inspect the cause

Otherwise, just `return err`. Wrapping at every line creates `"a: b: c: d: real error"` chains that are noise, not signal.

## Double-Wrapping Anti-Pattern

```go
// Bad
if err := svc.Do(); err != nil {
    return fmt.Errorf("svc.Do failed: %w", err) // adds no new info
}
```

If the wrap text duplicates the function name, drop it: `return err`.

```go
// Good
if err := svc.Do(ctx, userID); err != nil {
    return fmt.Errorf("svc.Do user=%d: %w", userID, err) // adds an input the caller does not have
}
```

## Multiple Wraps with `errors.Join`

Wrapping is a chain (one cause). When several independent operations failed, use `errors.Join` instead:

```go
var errs []error
for _, item := range items {
    if err := process(item); err != nil {
        errs = append(errs, fmt.Errorf("item %s: %w", item.ID, err))
    }
}
return errors.Join(errs...) // nil if errs is empty
```

`errors.Is` and `errors.As` walk every branch.

## Custom Unwrap

For custom error types, implement `Unwrap` so `errors.Is`/`As` can reach the cause:

```go
type RetryableError struct{ Cause error }

func (e *RetryableError) Error() string { return "retryable: " + e.Cause.Error() }
func (e *RetryableError) Unwrap() error { return e.Cause }
```

For multi-cause errors, implement `Unwrap() []error` (Go 1.20+):

```go
type MultiError struct{ Errs []error }

func (m *MultiError) Error() string   { /* concatenate */ }
func (m *MultiError) Unwrap() []error { return m.Errs }
```

`errors.Is`/`As` will descend into every returned error. This is what `errors.Join` does internally.

SHA-256: 86df3795d4ffb58cc02af9b9b09be6b2adb2bf5f6fd4e4476e3fe7f8a1ac6104