Error Handling — Junior¶
At junior level, focus on this question:
How can I apply Error Handling in one small example and prove the result?
Use the smallest realistic scenario that exposes the decision and its failure behavior.¶
Core Concepts¶
1. error is just an interface¶
Anything with an Error() string method satisfies it. This is why errors.New("boom") and a hand-written struct with an Error() method are both valid error values.
2. Check errors where they happen, not later¶
f, err := os.Open("file.txt")
if err != nil {
return err // handle immediately, don't keep going with a broken f
}
defer f.Close()
Continuing to use a value after ignoring its error (f, _ := os.Open(...)) is one of the most common sources of confusing downstream failures.
3. Wrap errors to preserve context without losing the original¶
%w (not %v or %s) wraps the original error so it remains inspectable later via errors.Is/errors.As, while the message adds context about where the failure happened.
4. Sentinel errors are compared, not string-matched¶
var ErrNotFound = errors.New("not found")
func Find(id string) (Item, error) {
if !exists(id) {
return Item{}, ErrNotFound
}
...
}
// caller
if errors.Is(err, ErrNotFound) {
// handle specifically
}
Never compare error messages with string equality — messages can change; sentinel identity via errors.Is is stable even through wrapping.
5. Custom error types carry structured data¶
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
// caller
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("bad field:", ve.Field)
}
errors.As extracts a specific error type (even through wrapping), giving access to fields the plain string message can't provide.
Code Examples¶
Example 1 — A wrapped error chain¶
func loadUser(id string) (User, error) {
row, err := db.QueryRow(id)
if err != nil {
return User{}, fmt.Errorf("loadUser(%s): %w", id, err)
}
...
}
If db.QueryRow returns sql.ErrNoRows, the caller can still detect it: errors.Is(err, sql.ErrNoRows) returns true even though the message now says "loadUser(42): sql: no rows in result set".
Example 2 — errors.Is vs. ==¶
_, err := loadUser("42")
if err == sql.ErrNoRows { // FALSE — err is wrapped, not identical
...
}
if errors.Is(err, sql.ErrNoRows) { // TRUE — unwraps the chain
...
}
Example 3 — Multiple custom error types¶
type NotFoundError struct{ ID string }
func (e *NotFoundError) Error() string { return "not found: " + e.ID }
type PermissionError struct{ User string }
func (e *PermissionError) Error() string { return "permission denied for " + e.User }
func handle(err error) {
var nf *NotFoundError
var pe *PermissionError
switch {
case errors.As(err, &nf):
respond(404, nf.Error())
case errors.As(err, &pe):
respond(403, pe.Error())
default:
respond(500, "internal error")
}
}
Best Practices¶
- Check every error at the call site; never discard with
_unless you have a specific, documented reason. - Wrap with
%w, not%v, when the caller might need to inspect the underlying error. - Use
errors.Isfor sentinel comparisons,errors.Asfor extracting a specific error type — never==or type assertions directly on a potentially-wrapped error. - Keep error messages lowercase and without trailing punctuation (Go convention) since they're often embedded in a larger wrapped message.
- Don't log and return the same error — that usually means it gets logged twice up the call stack. Pick one place to log fully.
Edge Cases & Pitfalls¶
%vinstead of%wsilently breakserrors.Is/errors.Asfor anyone downstream — the message looks identical, but wrapping is lost.- Comparing a wrapped error with
==always fails — useerrors.Is. - A custom error type with a pointer receiver on
Error()—errors.Asneeds a pointer-to-pointer target (var ve *ValidationError; errors.As(err, &ve)), a common syntax trip-up.
Common Mistakes¶
| Mistake | Fix |
|---|---|
_, _ = f.Close() — discarding errors reflexively | Handle or explicitly document why it's safe to ignore |
Using %v when wrapping was intended | Use %w |
Comparing wrapped errors with == | Use errors.Is |
Returning errors.New(fmt.Sprintf(...)) | Use fmt.Errorf directly — it's the same thing with less code |
Apply it¶
- Choose one small, known input for Error Handling.
- Predict the output or observable behavior.
- Run the smallest example or probe that exercises the concept.
- Change one input to trigger a failure or boundary case.
- Explain the evidence using the guide's vocabulary.
Verify your work¶
- Record the exact input, command or code path, and output.
- Repeat the probe and confirm the result is consistent.
- Show one expected success and one expected failure.
- Resolve any difference between the prediction and the evidence.
Review questions¶
- What problem does Error Handling solve in the example?
- Which input changes the observed result, and why?
- What is the smallest useful success check?
- Which beginner mistake would your evidence catch?
In this topic
- junior
- middle
- senior
- professional