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:
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:
var ErrNotFound = errors.New("not found")declares the original error at the package level.fmt.Errorf("load user 7: %w", ErrNotFound)puts it into an envelope with a remark.- The first
Printlnprints the whole text: the remark from the envelope and, after a colon, the original message. - The second prints
false:fmt.Errorfreturned a new error value—the envelope, notErrNotFound.
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:
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.
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)) // trueLet's walk through the example line by line:
erris an envelope markedload user 7:with the sentinel inside.errors.Is(err, ErrNotFound)opens the envelope, finds the sentinel and printstrue.againputserrinto one more envelope markedstart:.fmt.Println(again)prints the longer text:start: load user 7: not found.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.
err := ErrNotFound — a marker with the text "not found". Press the button to wrap it.
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:
err := fmt.Errorf("load user 7: %v", ErrNotFound) // ✗
fmt.Println(errors.Is(err, ErrNotFound)) // falseThe 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.Errorfwith%wputs the original error inside the new one, and that makes a chain.==compares only the outer error, whileerrors.Is(err, target)looks fortargetalong the whole chain.- Check a sentinel such as
ErrNotFoundwitherrors.Is, not with==or by its text. %vkeeps only the text, anderrors.Iscan 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.