Error Handling in Go: Wrapping, errors.Is and errors.As
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
paniconly 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.