Day 019 — Custom & Sentinel Errors; errors.Is / As / Join¶
Month 1 · Week 3 · ⬅ Day 018 · Day 020 ➡ · Journal index
🎯 Learning Objective¶
Design sentinel and custom error types and inspect error chains with errors.Is, errors.As, and errors.Join.
📚 Topics¶
- Sentinel errors (
var ErrX = errors.New(...)) · comparison witherrors.Is - Custom error types (structs) carrying fields · matched with
errors.As - Implementing
Unwrap() error(single and[]error) ·errors.Join - Sentinel vs custom: when to use which
📖 Reading / Sources¶
- Learning Go (Bodner) ch.9 — Sentinel/Custom errors,
Is/As - Go blog — Working with Errors in Go 1.13
-
errors.Is·errors.As·errors.Join - Dave Cheney — Don't just check errors, handle them gracefully
📝 Notes¶
- A sentinel error is a package-level value you compare against:
var ErrNotFound = errors.New("not found"). Callers test it witherrors.Is(err, ErrNotFound), which walks the%wchain — nevererr == ErrNotFound(that misses wrapped errors) → [[error-wrapping]]. - A custom error type is any type implementing
Error() string, usually a struct carrying structured fields:type ValidationError struct{ Field string; Err error }. Extract it witherrors.As(err, &target), which setstargetto the first matching error in the chain. - Give a custom type an
Unwrap() errormethod soerrors.Is/Ascan see through it to a wrapped cause. WithoutUnwrap, the chain stops at your type. errors.Is(err, target)→ "is this specific error (sentinel) anywhere in the chain?"errors.As(err, &t)→ "is there an error of this type in the chain? give it to me." UseIsfor known values,Asfor typed data.- A custom type can also define
Is(target error) boolto customize matching (semantic equality), but most types don't need it. errors.Join(errs...)(Go 1.20+) combines multiple errors into one whoseError()is newline-separated and whoseUnwrap() []errorletsIs/Asmatch any member. Great for accumulating validation failures.- Sentinel vs custom rule of thumb: sentinel when the fact of the failure is all the caller needs (
ErrNotFound); custom type when the caller needs data about it (which field, which line, an HTTP status). - Beware the nil-interface trap with custom errors: returning a typed nil
*ValidationErroraserroris non-nil → [[nil-interface-trap]]. Return literalnilfor success.
💻 Code Examples¶
var ErrNotFound = errors.New("not found")
type ValidationError struct {
Field string
Err error
}
func (e *ValidationError) Error() string { return e.Field + ": " + e.Err.Error() }
func (e *ValidationError) Unwrap() error { return e.Err } // let Is/As see the cause
func lookup(id string) error {
return fmt.Errorf("lookup %s: %w", id, ErrNotFound)
}
func main() {
err := lookup("u-42")
fmt.Println(errors.Is(err, ErrNotFound)) // true — sentinel found in chain
verr := &ValidationError{Field: "email", Err: ErrNotFound}
var ve *ValidationError
fmt.Println(errors.As(verr, &ve), ve.Field) // true email
all := errors.Join(ErrNotFound, &ValidationError{Field: "age", Err: errors.New("negative")})
fmt.Println(errors.Is(all, ErrNotFound)) // true — Join matches any member
}
Full code:
examples/month-01/errors/main.go· Run:go run ./examples/month-01/errors
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
ValidationError type + Unwrap, matched with errors.As |
✅ | exercises/month-01/week-3/validate |
ValidateUser accumulating failures with errors.Join |
✅ | exercises/month-01/week-3/validate |
🐛 Mistakes Made¶
- Compared with
err == ErrNotFoundafter wrapping with%w→ false.errors.Isfixed it by walking the chain. - Forgot
Unwrap()on my custom type, soerrors.Is(err, ErrNotFound)couldn't see the wrapped sentinel. - Used a value receiver for
Error()but a pointer forUnwrap()→ only*Thad the full method set; standardized on pointer receivers.
❓ Open Questions¶
- Should every exported package expose sentinels for its failure modes? (Expose the ones callers must branch on; keep internal ones private.)
🧠 Active Recall (answer without looking)¶
-
Q: When do you reach for
errors.Asinstead oferrors.Is?
A
When you need the *typed* error itself (to read its fields), not just to know whether a specific sentinel value is present. `As` extracts; `Is` compares. -
Q: What does
errors.Joingive you thatfmt.Errorf("%w")doesn't?
A
It combines *multiple* errors into one whose `Unwrap() []error` lets `errors.Is`/`As` match any of them — ideal for accumulating several validation failures.
🪶 Feynman Reflection¶
Think of an error chain as a set of nested boxes. A sentinel is a labelled box you ask "is this exact label somewhere inside?" (errors.Is). A custom type is a box with compartments holding data, and you ask "is there a box of this shape inside? hand it over so I can read its compartments" (errors.As). errors.Join ties several boxes into one bundle so a single question can find any of them.
🕳️ Knowledge Gaps¶
- Designing a clean public error API (which sentinels/types to export) — revisit during the REST-API project.
✅ Summary¶
I can design sentinel and custom errors, wire Unwrap so chains stay inspectable, and use errors.Is/As/Join to branch on and extract errors robustly.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 020:
panic,recover, and the narrow cases where they're appropriate.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(examples): sentinel/custom errors and errors.Is/As/Join (day 019)