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 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.

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.