Skip to content
Theory · Errors · lesson 6 of 11

Wrapping with %w and errors.Is

In the previous lesson the total function returned an error upward as is. Often you add context to it: what exactly the code was doing when things broke. Picture putting the original delivery note into an envelope and writing your own remark on the envelope: "while loading user 7". The remark is new, and the original note lies inside.

An error with context is created with fmt.Errorf and %w. Its text contains the original message, yet comparing it with == to the original error gives false:

GO
package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("not found")

func main() {
    err := fmt.Errorf("load user 7: %w", ErrNotFound)
    fmt.Println(err)                // load user 7: not found
    fmt.Println(err == ErrNotFound) // false
}

Let's walk through the example line by line:

  1. var ErrNotFound = errors.New("not found") declares the original error at the package level.
  2. fmt.Errorf("load user 7: %w", ErrNotFound) puts it into an envelope with a remark.
  3. The first Println prints the whole text: the remark from the envelope and, after a colon, the original message.
  4. The second prints false: fmt.Errorf returned a new error value—the envelope, not ErrNotFound.

Yet the calling code needs to find out which error happened, not only read the text.

Sentinel errors

The variable ErrNotFound in the example is declared at the package level. Such a variable is called a sentinel error: one known value by which code recognizes a particular failure. By convention its name starts with Err. A function returns the sentinel as is:

GO
func findUser(id int) (string, error) {
    if id != 1 {
        return "", ErrNotFound
    }
    return "ann", nil
}

A sentinel is recognized by its value, not by its text. Another error from errors.New("not found") is not equal to it, even though their texts match.

The error chain

Wrapping with %w puts the original error inside the new one. A wrapped error can be wrapped again, which makes a chain of errors—an envelope inside an envelope.

The function errors.Is(err, target) walks the chain from err inward and returns true if it meets target. The order of arguments matters:

  • first comes err—the error being checked;
  • second comes target—the sentinel searched for in it.
GO
err := fmt.Errorf("load user 7: %w", ErrNotFound)
fmt.Println(errors.Is(err, ErrNotFound)) // true
again := fmt.Errorf("start: %w", err)
fmt.Println(again)                         // start: load user 7: not found
fmt.Println(errors.Is(again, ErrNotFound)) // true

Let's walk through the example line by line:

  1. err is an envelope marked load user 7: with the sentinel inside.
  2. errors.Is(err, ErrNotFound) opens the envelope, finds the sentinel and prints true.
  3. again puts err into one more envelope marked start:.
  4. fmt.Println(again) prints the longer text: start: load user 7: not found.
  5. errors.Is(again, ErrNotFound) opens both envelopes in turn and finds the sentinel again.

Here the envelope picture ends: errors.Is does not look for a sheet with similar text, but for that very sentinel—the same value.

Press the button: it wraps the sentinel in two envelopes, then errors.Is opens them one by one—watch the error text grow and where the sentinel turns up.

LiveThe error chainlayers: 0 · errors.Is → …
ErrNotFound
err.Error() → "not found"

err := ErrNotFound — a marker with the text "not found". Press the button to wrap it.

Watch the error text grow

Three ways to check an error

Interviewers like to ask this: how does errors.Is differ from ==? To answer, let's compare three ways to recognize a sentinel.

Comparing the text

The expression err.Error() == "not found" compares the error's text with a string. Such a check breaks on the first edit of the message. Sentinels exist precisely so that the text can change without breaking anything.

Comparing with ==

The == operator compares only the outer error. Without wrapping it works: findUser(7) returns the very same value ErrNotFound, and err == ErrNotFound gives true. But once the sentinel is wrapped, == gives false, as in the lesson's first example.

The errors.Is function

errors.Is checks the whole chain. It works without wrapping too: for ErrNotFound itself, the chain consists of one error. So always check a sentinel with errors.Is, not with ==. A function that returns the sentinel as is today may wrap it tomorrow.

The standard library has sentinels too. The error from strconv.Atoi("x") wraps strconv.ErrSyntax, so errors.Is(err, strconv.ErrSyntax) is true for it, while err == strconv.ErrSyntax is false.

Next to errors.Is, interviewers often bring up errors.As. It searches the chain not for a particular value but for an error of a particular type—for example, your own type with an Error() method.

Common mistake

The context is added with %v instead of %w:

GO
err := fmt.Errorf("load user 7: %v", ErrNotFound) // ✗
fmt.Println(errors.Is(err, ErrNotFound))         // false

The error text is the same, load user 7: not found, but %v inserts only the string. It is like copying the note's text onto the envelope and throwing the note itself away. The original error is not inside, and errors.Is does not find it. To keep the chain, wrap with %w.

Key points

  • fmt.Errorf with %w puts the original error inside the new one, and that makes a chain.
  • == compares only the outer error, while errors.Is(err, target) looks for target along the whole chain.
  • Check a sentinel such as ErrNotFound with errors.Is, not with == or by its text.
  • %v keeps only the text, and errors.Is can no longer find the original error.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way