5. Error Handling

Design clear error contracts; wrap with context; make failures actionable and diagnosable.

Question: What is the difference between a sentinel error and a typed error? When should you use each?

Answer:

  • Sentinel Error: A specific, exported error value, like io.EOF or sql.ErrNoRows. They are compared by value (errors.Is(err, sql.ErrNoRows)).

  • Typed Error: A custom error struct that implements the error interface. They are checked by type (errors.As(err, &myErr)), allowing you to inspect their fields for more context.

Explanation: Use sentinel errors for simple, fixed error conditions. Prefer typed errors when you need to provide more context about the failure, such as a status code, a timeout flag, or other metadata that the caller can programmatically inspect. Avoid exporting too many sentinel errors from a package, as it can bloat the API.

// Example: typed error with errors.Is
var ErrNotFound = errors.New("not found")

type TemporaryError struct{ Err error }
func (e TemporaryError) Error() string { return e.Err.Error() }
func (e TemporaryError) Unwrap() error { return e.Err }

func find(id string) (Item, error) {
    // ...
    return Item{}, fmt.Errorf("find %s: %w", id, ErrNotFound)
}

func handler(w http.ResponseWriter, r *http.Request) {
    _, err := find("42")
    if err != nil {
        switch {
        case errors.Is(err, ErrNotFound):
            http.Error(w, "not found", http.StatusNotFound)
        case errors.As(err, new(TemporaryError)):
            http.Error(w, "try again", http.StatusServiceUnavailable)
        default:
            http.Error(w, "internal", http.StatusInternalServerError)
        }
        return
    }
}

Question: How should you wrap and propagate errors?

Answer: Wrap errors with context using %w and inspect with errors.Is/As. Return errors upward; avoid logging and returning the same error at multiple layers.

Explanation: Double-logging creates noisy logs. Log at the boundary (e.g., HTTP handler, CLI) with sufficient context and map errors to user-facing messages or status codes there.

Question: What is errors.Join and when is it useful?

Answer: errors.Join combines multiple errors into one. errors.Is/As work across the joined set.

Explanation: Useful when multiple cleanup steps can fail or when aggregating parallel errors. Preserve the original errors to avoid losing diagnostics.

Question: How do context.WithCancelCause and context.Cause help error propagation?

Answer: They attach a concrete cause to cancellation so callers can log/report the underlying reason instead of generic context canceled.

Explanation: Distinguish timeouts (DeadlineExceeded) from domain cancellations.