errors.Is vs errors.As — Tasks¶
Hands-on exercises. Each comes with a problem statement, hints, and a reference solution. Difficulty: easy → hard.
Task 1 (Easy) — Match a wrapped sentinel¶
Write a function that wraps io.EOF with fmt.Errorf and a context message, then verifies the wrapped value is still recognizable via errors.Is.
Hints - Use %w, not %v. - errors.Is(err, io.EOF) should be true.
Solution
package main
import (
"errors"
"fmt"
"io"
)
func main() {
inner := io.EOF
err := fmt.Errorf("read header: %w", inner)
fmt.Println(err == io.EOF) // false (the wrapper is not io.EOF)
fmt.Println(errors.Is(err, io.EOF)) // true (chain walk finds it)
}
Task 2 (Easy) — Extract *os.PathError¶
Write a program that opens a non-existent file and uses errors.As to extract the *os.PathError, then prints the path that failed.
Hints - os.Open returns *os.PathError for I/O failures. - Declare the variable as var pe *os.PathError, not var pe os.PathError.
Solution
package main
import (
"errors"
"fmt"
"os"
)
func main() {
_, err := os.Open("/no/such/file")
var pe *os.PathError
if errors.As(err, &pe) {
fmt.Printf("op=%s path=%s\n", pe.Op, pe.Path)
} else {
fmt.Println("no PathError in chain")
}
}
Task 3 (Easy) — Define your own sentinel¶
Define a package-level sentinel ErrInsufficientFunds and a function withdraw(balance, amount int) that returns it when the amount exceeds the balance. In main, wrap the result with a context and recover the sentinel via errors.Is.
Hints - Use errors.New for the sentinel. - Wrap the return value in main with fmt.Errorf("transfer failed: %w", err).
Solution
package main
import (
"errors"
"fmt"
)
var ErrInsufficientFunds = errors.New("insufficient funds")
func withdraw(balance, amount int) error {
if amount > balance {
return ErrInsufficientFunds
}
return nil
}
func main() {
err := withdraw(50, 100)
err = fmt.Errorf("transfer: %w", err)
if errors.Is(err, ErrInsufficientFunds) {
fmt.Println("declined: not enough money")
}
}
Task 4 (Easy) — Define a typed error and extract it¶
Define a *ValidationError with fields Field and Message. Write a validate(name, email string) function that returns one. Wrap the result twice with fmt.Errorf and recover it with errors.As.
Hints - Implement Error() on the pointer receiver. - Variable target: var ve *ValidationError.
Solution
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("invalid %s: %s", e.Field, e.Message)
}
func validate(name, email string) error {
if name == "" {
return &ValidationError{Field: "name", Message: "required"}
}
if email == "" {
return &ValidationError{Field: "email", Message: "required"}
}
return nil
}
func main() {
err := validate("", "x@x")
err = fmt.Errorf("user create: %w", err)
err = fmt.Errorf("api request: %w", err)
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("validation: field=%s msg=%s\n", ve.Field, ve.Message)
}
}
Task 5 (Easy) — Walk the chain manually¶
Write a function printChain(err error) that uses errors.Unwrap to print every error in a chain, one per line.
Hints - Stop when errors.Unwrap returns nil. - Print the outer error first.
Solution
package main
import (
"errors"
"fmt"
"io"
)
func printChain(err error) {
for err != nil {
fmt.Println(err)
err = errors.Unwrap(err)
}
}
func main() {
e := fmt.Errorf("a: %w", fmt.Errorf("b: %w", io.EOF))
printChain(e)
}
Task 6 (Medium) — Custom Is for kind matching¶
Define a typed error with a Kind field. Implement an Is method so that errors.Is(err, MyKind) returns true when err.Kind == MyKind. Test it with two different kind sentinels.
Hints - The receiver compares target against its own kind. - Define both kinds as package-level errors.
Solution
package main
import (
"errors"
"fmt"
)
var (
KindNotFound = errors.New("not_found")
KindForbidden = errors.New("forbidden")
)
type AppErr struct {
Kind error
Op string
Err error
}
func (e *AppErr) Error() string { return fmt.Sprintf("%s: %v", e.Op, e.Err) }
func (e *AppErr) Unwrap() error { return e.Err }
func (e *AppErr) Is(target error) bool { return target == e.Kind }
func main() {
err := &AppErr{Kind: KindNotFound, Op: "get-user", Err: errors.New("no row")}
fmt.Println(errors.Is(err, KindNotFound)) // true
fmt.Println(errors.Is(err, KindForbidden)) // false
}
Task 7 (Medium) — errors.Join and detection¶
Combine three errors with errors.Join and write asserts that errors.Is finds each individual one.
Hints - Define each as a sentinel. - Use errors.Is(joined, X) for each X.
Solution
package main
import (
"errors"
"fmt"
)
var (
ErrA = errors.New("A failed")
ErrB = errors.New("B failed")
ErrC = errors.New("C failed")
)
func main() {
joined := errors.Join(ErrA, ErrB, ErrC)
fmt.Println(errors.Is(joined, ErrA)) // true
fmt.Println(errors.Is(joined, ErrB)) // true
fmt.Println(errors.Is(joined, ErrC)) // true
fmt.Println(errors.Is(joined, errors.New("X"))) // false (different pointer)
}
Task 8 (Medium) — Custom As to extract a code¶
Define a typed error with an int code. Implement an As method that, when given a *int target, writes the code into it.
Hints - The As method uses a type switch on target. - For non-matching types, return false; do not write.
Solution
package main
import (
"errors"
"fmt"
)
type CodedErr struct {
Code int
Msg string
}
func (e *CodedErr) Error() string { return e.Msg }
func (e *CodedErr) As(target any) bool {
if t, ok := target.(*int); ok {
*t = e.Code
return true
}
return false
}
func main() {
err := &CodedErr{Code: 42, Msg: "boom"}
err2 := fmt.Errorf("op: %w", err)
var code int
if errors.As(err2, &code) {
fmt.Println("code:", code)
}
var ce *CodedErr
if errors.As(err2, &ce) {
fmt.Println("msg:", ce.Msg)
}
}
Task 9 (Medium) — Translate at a boundary¶
Two packages: repo returns repo.ErrNotFound. The svc package wraps repo and returns svc.ErrNotFound. Make errors.Is(err, svc.ErrNotFound) work for any caller of svc, but only when repo returned repo.ErrNotFound.
Hints - Use a type switch to translate at the boundary. - Wrap with %w to preserve the chain.
Solution
package main
import (
"errors"
"fmt"
)
// repo
var ErrRepoNotFound = errors.New("repo: not found")
func repoGet(id int) error {
return ErrRepoNotFound
}
// svc
var ErrSvcNotFound = errors.New("svc: not found")
func svcGet(id int) error {
err := repoGet(id)
if errors.Is(err, ErrRepoNotFound) {
return fmt.Errorf("%w (cause: %v)", ErrSvcNotFound, err)
}
return err
}
func main() {
err := svcGet(7)
fmt.Println(errors.Is(err, ErrSvcNotFound)) // true
fmt.Println(errors.Is(err, ErrRepoNotFound)) // false (wrapped only the svc sentinel)
}
Task 10 (Medium) — Round-trip an error type¶
Write a function that produces an error chain four levels deep ending in a typed *MyErr. Recover it with errors.As. Confirm that errors.Is matches a sentinel kind defined inside *MyErr's Is method.
Hints - Combine the patterns from Tasks 6 and 4.
Solution
package main
import (
"errors"
"fmt"
)
var KindBoom = errors.New("kind_boom")
type MyErr struct {
Kind error
Note string
}
func (e *MyErr) Error() string { return e.Note }
func (e *MyErr) Is(target error) bool { return target == e.Kind }
func deepFail() error {
return &MyErr{Kind: KindBoom, Note: "the deep one"}
}
func wrap(err error, msg string) error {
return fmt.Errorf("%s: %w", msg, err)
}
func main() {
err := wrap(wrap(wrap(wrap(deepFail(), "lvl4"), "lvl3"), "lvl2"), "lvl1")
fmt.Println(errors.Is(err, KindBoom)) // true
var me *MyErr
if errors.As(err, &me) {
fmt.Println(me.Note) // "the deep one"
}
}
Task 11 (Hard) — Write a category-aware HTTP middleware¶
Define kind sentinels KindNotFound, KindBadInput, KindInternal. Define an AppErr type with custom Is. Write an HTTP handler middleware that translates these kinds to status codes 404/400/500.
Hints - Use errors.Is(err, KindX) in a switch. - Default case: 500.
Solution
package main
import (
"errors"
"fmt"
"net/http"
)
var (
KindNotFound = errors.New("not_found")
KindBadInput = errors.New("bad_input")
KindInternal = errors.New("internal")
)
type AppErr struct {
Kind error
Op string
Err error
}
func (e *AppErr) Error() string { return fmt.Sprintf("%s: %v", e.Op, e.Err) }
func (e *AppErr) Unwrap() error { return e.Err }
func (e *AppErr) Is(target error) bool { return target == e.Kind }
func httpStatus(err error) int {
switch {
case errors.Is(err, KindNotFound):
return http.StatusNotFound
case errors.Is(err, KindBadInput):
return http.StatusBadRequest
default:
return http.StatusInternalServerError
}
}
type appHandler func(http.ResponseWriter, *http.Request) error
func (h appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h(w, r); err != nil {
http.Error(w, err.Error(), httpStatus(err))
}
}
func main() {
h := appHandler(func(w http.ResponseWriter, r *http.Request) error {
return &AppErr{Kind: KindNotFound, Op: "get-user", Err: errors.New("user 7 not found")}
})
mux := http.NewServeMux()
mux.Handle("/", h)
fmt.Println("would listen on :8080 (skipped)")
_ = mux
}
Task 12 (Hard) — Build a multi-error walker¶
Write a function walk(err error, fn func(error)) that calls fn on every error in the chain, including each leaf of multi-error trees. Use it on the result of errors.Join.
Hints - Type-assert interface{ Unwrap() []error } and interface{ Unwrap() error }. - Recurse into each child.
Solution
package main
import (
"errors"
"fmt"
)
func walk(err error, fn func(error)) {
if err == nil {
return
}
fn(err)
switch x := err.(type) {
case interface{ Unwrap() error }:
walk(x.Unwrap(), fn)
case interface{ Unwrap() []error }:
for _, c := range x.Unwrap() {
walk(c, fn)
}
}
}
func main() {
e := errors.Join(
fmt.Errorf("step1: %w", errors.New("a")),
fmt.Errorf("step2: %w", errors.New("b")),
errors.New("c"),
)
count := 0
walk(e, func(err error) {
count++
fmt.Println(count, "->", err)
})
}
Task 13 (Hard) — Detect a cycle without crashing¶
Write a safeIs function that behaves like errors.Is but detects cycles in Unwrap and returns false instead of looping forever.
Hints - Track visited nodes in a map[error]struct{}. - Use == to compare visited entries (works on interface values).
Solution
package main
import (
"errors"
"fmt"
)
func safeIs(err, target error) bool {
visited := make(map[error]struct{})
for err != nil {
if _, ok := visited[err]; ok {
return false
}
visited[err] = struct{}{}
if err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
u, ok := err.(interface{ Unwrap() error })
if !ok {
return false
}
err = u.Unwrap()
}
return false
}
type cyclic struct{ n int }
func (c *cyclic) Error() string { return fmt.Sprintf("c%d", c.n) }
func (c *cyclic) Unwrap() error { return c }
func main() {
c := &cyclic{n: 1}
fmt.Println(safeIs(c, errors.New("x"))) // false (would have hung with errors.Is)
}
(Note: avoid actually running errors.Is(c, ...) — it loops.)
Task 14 (Hard) — Re-implement errors.As with generics¶
Write AsT[T error](err error) (T, bool) that returns the first error of type T in the chain.
Hints - Use a generic type parameter constrained by error. - Use the existing errors.As internally.
Solution
package main
import (
"errors"
"fmt"
"os"
)
func AsT[T error](err error) (T, bool) {
var t T
if errors.As(err, &t) {
return t, true
}
return t, false
}
func main() {
_, err := os.Open("/no/such/file")
if pe, ok := AsT[*os.PathError](err); ok {
fmt.Println("path:", pe.Path)
}
}
The generic shim is more ergonomic but does not work for pointer-to-interface targets without further work (interface types do not satisfy the error constraint).
Task 15 (Hard) — Build a kind-counting test helper¶
Write a helper CountKinds(err error, kinds ...error) map[error]int that returns how many times each kind appears anywhere in err's chain (using errors.Is semantics on each subtree node).
Hints - Use the walk function from Task 12. - Increment the map only for nodes that match each kind.
Solution
package main
import (
"errors"
"fmt"
)
var KindA = errors.New("kindA")
var KindB = errors.New("kindB")
type ke struct{ kind error }
func (e *ke) Error() string { return e.kind.Error() }
func (e *ke) Is(target error) bool { return target == e.kind }
func walk(err error, fn func(error)) {
if err == nil {
return
}
fn(err)
switch x := err.(type) {
case interface{ Unwrap() error }:
walk(x.Unwrap(), fn)
case interface{ Unwrap() []error }:
for _, c := range x.Unwrap() {
walk(c, fn)
}
}
}
func CountKinds(err error, kinds ...error) map[error]int {
out := make(map[error]int)
walk(err, func(e error) {
for _, k := range kinds {
if errors.Is(e, k) {
out[k]++
}
}
})
return out
}
func main() {
j := errors.Join(
&ke{kind: KindA},
&ke{kind: KindB},
&ke{kind: KindA},
)
counts := CountKinds(j, KindA, KindB)
fmt.Println(counts)
}