Stage 1 · Code
Language Depth II
Errors in Depth
Go past basic `if err != nil` checks and learn how Go models known failures, adds context layer by layer, and still lets you inspect the real cause at the top.
Known Failures and Sentinel Errors
Some failures are not random surprises. They are named situations your program expects to recognize: 'record missing,' 'permission denied,' 'cache empty.' A sentinel error is a package-level error value created once and reused whenever that exact situation occurs.
The win is stability. If every call site builds its own string like 'not found', callers are forced to compare fragile text. A sentinel error gives the codebase one shared value to point at instead.
Sentinel errors are useful when callers need to branch on a known failure category. They are not special because they are global. They are special because they give the codebase one stable identity for that failure.
Wrapping Keeps the Story
Low-level code knows one part of the truth. Higher-level code knows another. Suppose a storage layer knows 'user not found', while a service layer knows that missing user mattered during invoice generation. Good error handling keeps both facts instead of flattening everything into one vague sentence.
fmt.Errorf with %w adds context while preserving the original error underneath. Without %w, you only keep a prettier string. With %w, you keep a chain.
Tracing an Error Chain
Let's build a realistic chain across several layers. Imagine a request to render a dashboard. The repository layer cannot find the account. The service layer adds account-specific context. The HTTP layer adds request-specific context. At the top, we still want to know the root cause was 'account missing.'
A healthy pattern is: low and middle layers add context, the boundary layer chooses the response. That keeps the chain informative without making every helper function responsible for final policy.
Custom Error Types
Sometimes a sentence is not enough. Maybe the caller needs a status code, a field name, an endpoint, or a retry hint. That is when a custom error type helps. A custom type still satisfies the error interface by implementing Error() string, but it can carry extra structured fields too.
Using errors.Is and errors.As
Now we can separate two related but different questions. errors.Is asks, 'does this chain contain this known error value somewhere inside?' errors.As asks, 'does this chain contain an error of this particular type, and if so, can I have it?'
If your code needs to make decisions, comparing strings like if err.Error() == ... is brittle. Prefer errors.Is for known sentinel values and errors.As for typed errors with structured fields.
Quiz
Why create a sentinel error like `ErrUserNotFound` instead of returning a fresh `errors.New("user not found")` each time?
Practice
Create a sentinel error `ErrClosedTicket = errors.New("ticket closed")`. Write a function `CloseTicket(id int) error` that returns `fmt.Errorf("close ticket %d: %w", id, ErrClosedTicket)` when `id` is `0`, and returns `nil` otherwise.
Summary
- Sentinel errors give known failure categories one stable shared identity.
- Wrapping with
%wadds context without throwing away the original cause. - An error chain can collect useful story details across several call layers.
- Custom error types carry structured fields when a plain sentence is not enough.
- Use
errors.Isfor known error values anderrors.Asfor concrete error types.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.