go 53 lines · 8 steps

Wrapping and inspecting errors in Go

How sentinel errors, custom error types, and wrapping let callers react to failures precisely.

Explained by highlit
1package store
2 
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7)
8 
9var ErrNotFound = errors.New("record not found")
10 
11type QueryError struct {
12 Query string
13 Err error
14}
15 
16func (e *QueryError) Error() string {
17 return fmt.Sprintf("query %q failed: %v", e.Query, e.Err)
18}
19 
20func (e *QueryError) Unwrap() error {
21 return e.Err
22}
23 
24func (s *Store) FindUser(id int64) (*User, error) {
25 const query = "SELECT id, email FROM users WHERE id = $1"
26 
27 var u User
28 err := s.db.QueryRow(query, id).Scan(&u.ID, &u.Email)
29 if err != nil {
30 if errors.Is(err, sql.ErrNoRows) {
31 return nil, fmt.Errorf("user %d: %w", id, ErrNotFound)
32 }
33 return nil, &QueryError{Query: query, Err: err}
34 }
35 
36 return &u, nil
37}
38 
39func (s *Store) ResolveUser(id int64) (*User, error) {
40 u, err := s.FindUser(id)
41 if err != nil {
42 if errors.Is(err, ErrNotFound) {
43 return s.guestUser(), nil
44 }
45 
46 var qe *QueryError
47 if errors.As(err, &qe) {
48 s.log.Printf("db failure on %q: %v", qe.Query, qe.Err)
49 }
50 return nil, err
51 }
52 return u, nil
53}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Wrapping an error with %w preserves the original so callers can match it later with errors.Is or errors.As.
  2. 2Implementing Unwrap on a custom error type plugs it into Go's standard error-inspection chain.
  3. 3Distinguishing 'not found' from 'real failure' lets callers degrade gracefully instead of treating every error the same.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Wrapping and inspecting errors in Go — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code