6. Error Handling
Return error
values, wrap with context, compare with errors.Is/As
, and recover only at safe boundaries.
Question: How does error wrapping work in Go, and why is it useful?
Answer: Error wrapping allows you to create a new error that contains a previous error, forming a chain. This is done using the %w
verb in fmt.Errorf
. It is useful for adding context to an error while preserving the original error for inspection.
Explanation: For example, a repository layer might get a "connection refused" error. The service layer can wrap that error with "failed to get user" context. The caller can then use errors.Is
to check if the error chain contains a specific sentinel error (like sql.ErrNoRows
), or errors.As
to extract a specific error type for more detailed inspection. This provides full context for logging while allowing programmatic inspection of the error cause.
func findUser(id int) error {
err := db.Query(id) // Assume this returns sql.ErrNoRows
if err != nil {
// Add context while preserving the original error
return fmt.Errorf("finding user %d: %w", id, err)
}
return nil
}
// Later, in the caller...
err := findUser(1)
if errors.Is(err, sql.ErrNoRows) {
// Handle the "not found" case specifically
}
Question: What are sentinel errors and how do you compare errors?
Answer: Sentinel errors are var
values (e.g., var ErrNotFound = errors.New("...")
). Compare using errors.Is
rather than ==
when wrapping may occur.
Explanation: Avoid matching on error strings. Wrap with %w
to retain the cause.
Question: What is
errors.Join
and when is it useful?
Answer: errors.Join
combines multiple errors into one. Use errors.Is/As
to test against members.
err := errors.Join(errA, errB)
if errors.Is(err, errA) { /* handle */ }
Question: When should you
recover
?
Answer: Only at safe boundaries (e.g., goroutine top-level) to convert panics into errors; never to mask programming bugs silently.
Question: When should you use
%w
vs%v
infmt.Errorf
?
Answer: Use %w
to wrap an error so callers can inspect with errors.Is/As
; use %v
to format without wrapping.
Explanation: Wrapping preserves the cause chain. Only one %w
is allowed per format.
Question: How do you define and expose custom error types?
Answer: Implement Error() string
on a type for rich errors, and export sentinels or constructors for comparison.
Explanation: Typed errors enable errors.As
in callers.
type NotFoundError struct{ ID int }
func (e NotFoundError) Error() string { return fmt.Sprintf("not found: %d", e.ID) }
var ErrUnauthorized = errors.New("unauthorized")
Question: How do you unwrap nested errors?
Answer: errors.Unwrap(err)
returns the next inner error; repeat to traverse the chain.
Explanation: Prefer errors.Is/As
for matching specific causes.