6. Error Handling
Return error values, wrap with context, compare with errors.Is/As, and recover only at safe boundaries.
Q1 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
}
Q2 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.
Q3 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 */ }
Q4 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.
Q5 When should you use %w vs %v in fmt.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.
Q6 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")
Q7 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.