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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Wrapping an error with %w preserves the original so callers can match it later with errors.Is or errors.As.
- 2Implementing Unwrap on a custom error type plugs it into Go's standard error-inspection chain.
- 3Distinguishing 'not found' from 'real failure' lets callers degrade gracefully instead of treating every error the same.
Related explainers
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/wrapping-and-inspecting-errors-in-go-explained-go-0b02/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.