Articles

The Go error pattern I actually use (part 5)

Five years of writing Go for production. Here's the error-handling shape that survived contact with on-call.

1 min readby Site Admin

What I do#

Three rules:

  1. Wrap at boundaries. Every error crossing a layer (handler → service → repo) gets a fmt.Errorf("<verb>: %w", err).
  2. Sentinel errors are the API. Define ErrNotFound, ErrInvalid at the service layer. Handlers check with errors.Is.
  3. Log once, at the top. Recoverer middleware logs the chain; lower layers don't slog.Error and re-return.

Example#

go
// service
func (s *Posts) GetBySlug(ctx context.Context, slug string) (*Post, error) {
    p, err := s.repo.GetBySlug(ctx, slug)
    if errors.Is(err, pgx.ErrNoRows) {
        return nil, fmt.Errorf("get post %q: %w", slug, ErrNotFound)
    }
    if err != nil { return nil, fmt.Errorf("get post: %w", err) }
    return p, nil
}

// handler
if err != nil { httpx.MapError(w, err); return }

What I stopped doing#

  • Wrapping at every call site. The stack is already in the trace.
  • Custom error types with stack fields. %w chains plus errors.Is is enough.
  • pkg/errors. The stdlib finally has what it gave us.
The Go error pattern I actually use (part 5) — settingserver.com