Error Handling in Go: Wrapping, errors.Is and errors.As

A
Author
28 Aug, 2026 1 min read 512 views

In Go, errors are ordinary values returned alongside results. The pattern is repetitive by design: it keeps every failure path visible.

The basic pattern

f, err := os.Open("config.json")
if err != nil {
    return fmt.Errorf("open config: %w", err)
}
defer f.Close()

%w wraps the original error, adding context while keeping the original available for inspection.

Sentinel errors and errors.Is

var ErrNotFound = errors.New("not found")

func (s *Store) User(id int) (*User, error) {
    u, ok := s.users[id]
    if !ok {
        return nil, fmt.Errorf("user %d: %w", id, ErrNotFound)
    }
    return u, nil
}

// Caller
u, err := store.User(42)
if errors.Is(err, ErrNotFound) {      // works through any number of wraps
    http.Error(w, "user not found", http.StatusNotFound)
    return
}

Custom error types and errors.As

type ValidationError struct {
    Field, Message string
}

func (e *ValidationError) Error() string {
    return e.Field + ": " + e.Message
}

// Caller
var ve *ValidationError
if errors.As(err, &ve) {
    respondJSON(w, 422, map[string]string{"field": ve.Field, "error": ve.Message})
    return
}

Combining errors

err := errors.Join(closeDB(), closeCache(), flushLogs()) // nil if all are nil

Guidelines

  • Add context that helps debugging ("load user 42: query: connection refused"), but don't repeat what the wrapped error already says.
  • Handle an error or return it, not both (logging and returning causes duplicate logs).
  • Compare with errors.Is/errors.As, never by string.
  • Use panic only for programmer bugs and impossible states, not for expected failures.
Was this article helpful?

Comments

No comments yet. Have a question or a tip? Be the first to share it.

Leave a comment

Comments are reviewed before they appear.