Skip to content

Day 018 — Errors as Values & Wrapping (%w)

Month 1 · Week 3 · ⬅ Day 017 · Day 019 ➡ · Journal index

🎯 Learning Objective

Treat errors as ordinary values, handle them explicitly, and build informative error chains by wrapping with fmt.Errorf("...: %w", err).

📚 Topics

  • error is an interface · the (value, error) return idiom
  • Handle, don't ignore · don't panic for ordinary failures
  • Wrapping with %w vs formatting with %v · errors.Unwrap
  • Adding context at each layer without leaking implementation

📖 Reading / Sources

📝 Notes

  • error is just an interface: type error interface{ Error() string }. Errors are values you compare, wrap, and pass around — not exceptions → [[errors-are-values]].
  • The idiom is the multi-return (result, error). Check it immediately: if err != nil { return ..., err }. Never silently ignore an error with _ unless you can articulate why.
  • Add context as you go up the stack so the final message reads like a path: read config: open /etc/app.yaml: no such file or directory. Each layer prepends what it was doing.
  • Wrap with the %w verb: fmt.Errorf("read config: %w", err). This stores the original error so callers can later errors.Is/errors.As/errors.Unwrap it → [[error-wrapping]] (Day 019).
  • Use %v (or %s) when you want to flatten the message and deliberately break the chain — e.g. to avoid leaking an internal sentinel across an API boundary.
  • A %w-wrapped error implements Unwrap() error; errors.Unwrap(err) peels one layer. You rarely call it directly — errors.Is/As walk the chain for you.
  • Don't double-decorate: avoid failed to ... prefixes on every layer (the word "error/failed" repeats noisily). Keep context terse and lowercase, no trailing punctuation (Go style: error strings are not capitalized and don't end with punctuation).
  • For functions that take a context.Context, it's the first parameter (ctx context.Context), and its ctx.Err() is itself an error you wrap/return → [[context-first-param]].

💻 Code Examples

func readConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        // %w keeps the original error in the chain for errors.Is/As later.
        return nil, fmt.Errorf("read config %q: %w", path, err)
    }
    return data, nil
}

func main() {
    _, err := readConfig("/no/such/file")
    fmt.Println(err)                         // read config "/no/such/file": open ...: no such file or directory
    fmt.Println(errors.Is(err, os.ErrNotExist)) // true — the wrapped os error is still reachable
}

Full code: examples/month-01/errors/main.go · Run: go run ./examples/month-01/errors

🏋️ Exercises / Practice

Exercise Status Link
ParseConfig wrapping strconv/IO errors with %w exercises/month-01/week-3/validate
Round-trip: wrap then errors.Is the original exercises/month-01/week-3/validate

🐛 Mistakes Made

  • Used %v to wrap, then errors.Is(err, os.ErrNotExist) returned false because the chain was broken. Switched to %w.
  • Capitalized an error string ("Failed to open") — go vet/style says error strings should be lowercase, no trailing period.

❓ Open Questions

  • How much context is too much? (Each layer adds one short clause about its operation; don't repeat the layer below.)

🧠 Active Recall (answer without looking)

  1. Q: What's the difference between wrapping with %w and %v in fmt.Errorf?

    A `%w` preserves the original error in the chain (so `errors.Is`/`As`/`Unwrap` can find it); `%v` only formats the message text and breaks the chain.

  2. Q: What is the error type, fundamentally?

    A An interface with one method: `Error() string`. Errors are ordinary values returned alongside results, not thrown exceptions.

🪶 Feynman Reflection

In Go an error is just a value you carry back from a function, like a receipt that says "this went wrong, here's why." Wrapping with %w staples each layer's note onto the receipt without throwing away the layers underneath, so the caller can read the whole story — or programmatically find a specific note deep in the stack.

🕳️ Knowledge Gaps

  • Deciding which boundaries should wrap vs flatten errors — clearer after Day 019's sentinel/custom errors.

✅ Summary

I handle errors as values returned from functions, check them immediately, and wrap with %w to build readable, machine-inspectable error chains.

⏭️ Next Steps / Prep for Tomorrow

  • Day 019: sentinel and custom errors, plus errors.Is, errors.As, and errors.Join.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: feat(examples): errors as values and %w wrapping (day 018)