Before you begin
Why this matters
Go code checks for an error after almost every call that can fail. If you can follow an error value through each return, you can find the line where an error was dropped, and explain why a check such as err == ErrNoCoat stopped matching after someone added context to a message.
What this helps you reason about
- Follow an error back through each return, and say which lines are skipped.
- Spot the three ways an error gets lost: discarding the error with the blank identifier _, declaring a new err with := inside an inner block, and adding context with %v where %w was needed.
- Choose between a sentinel error and an error type, and check either one with errors.Is or errors.As.
In Java and Python, a failing call throws an exception, and the exception leaves each function without any code of yours until a catch or except receives it. In Rust, a function returns a Result, and ? passes the error on. Go does neither. An error is an ordinary value, returned as the last result, and every function on the way has to check it and return it again. This lesson follows one error from the function that makes it to the code that decides what to do, and shows how the error keeps its identity while each step adds context.
Already know this? The short version
- An error is an interface value with an Error() string method, returned as the last result; nil means success, and an error goes back to the calling code only through a return statement that someone wrote.
- Three ways to lose an error: _ discards it; v, err := in an inner block declares a new err that hides the outer one; and fmt.Errorf with %v copies only the message, so the original error is gone from the chain.
- Wrap with fmt.Errorf("context: %w", err) when the calling code may need to check the cause, and use %v when the cause is an internal detail; check with errors.Is(err, ErrX), not ==, which sees only the outermost error, and get details with errors.As(err, &target), where target has exactly the type that was returned, *MyError or MyError.
- A nil *MyError returned as error is not nil, because the interface holds a type; return nil itself on success. The section “What goes wrong with errors in real programs?” covers handling an error once, keeping the error from a deferred Close, and checking context errors with errors.Is.
Words in this lesson (7)
- interface
- A type defined by a list of methods. A variable of an interface type can hold a value of any type that has those methods, together with the name of that type.
- error
- Go’s built-in interface for failures. Any type with an Error() string method is an error. A function reports a failure by returning a non-nil error as its last result; nil means no failure.
- shadowing
- Declaring a variable in an inner block with the same name as an outer one. The inner variable hides the outer one until the block ends.
- sentinel error
- An error value made once and stored in a package-level variable, such as io.EOF, so that the code that receives an error can recognize that particular failure by comparing values.
- wrapping
- Making a new error that adds text to an existing error and keeps the existing error inside it, with fmt.Errorf and the %w verb. The errors inside each other form a chain.
- errors.Is and errors.As
- Functions that search an error and every error wrapped inside it. errors.Is looks for one particular value; errors.As looks for an error of one type and stores it in a variable you pass.
- error type
- A type you define with an Error() string method, usually a struct whose fields carry details of the failure, such as which tag had no coat.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Error values
What is an error in Go?
A Go function that can fail returns an error as its last result, next to its normal results, as Function calls showed with (int, error). The code that called the function receives the error like any other value, and decides what to do with it.
error is an interface: a type defined by a list of methods. The error interface has one method, Error() string, which returns the message. A value of any type with that method can be stored in an error variable. errors.New("no coat for this tag") makes a simple error with that message. The value nil means there is no error.
The code that called the function checks the error right after the call. if err != nil { return "", err } returns early, so the rest of the function does not run, and the error becomes this function’s result. Each function on the way must return the error, or the error goes no further.
func handOver(tag int) (string, error) {
coat, err := findCoat(tag)
if err != nil {
// stop here and pass the error on
return "", err
}
return "here is your " + coat, nil
} An error is a value returned as the last result; each function must return it to pass it on.
Precisely: the other results, and panic
When a function returns a non-nil error, its other results are usually zero values, and the calling code should not use them. A few functions document an exception: an io.Reader’s Read may return n > 0 bytes together with io.EOF, and those bytes are valid. Go also has panic, which does unwind the call stack like an exception and can be stopped with recover in a deferred function. Go uses panic for programming mistakes, such as an index out of range, and returns errors for failures a program should expect.
Experiment
Follow an error back through two calls
findCoat returns an error value instead of a coat. Each function checks the value it received and returns early, so the error moves up one frame for each return statement, and the code after each if is skipped.
Why it matters An error goes back only as far as the code returns it. To find where an error went, follow the return statements, not a catch block.
Predict which lines of handOver and main run for tag 3, and what prints. Then change handOver(3) to handOver(1) and predict again.
package main
import (
"errors"
"fmt"
)
func findCoat(tag int) (string, error) {
if tag == 1 {
return "blue scarf", nil
}
return "", errors.New("no coat for this tag")
}
func handOver(tag int) (string, error) {
coat, err := findCoat(tag)
if err != nil {
return "", err
}
return "here is your " + coat, nil
}
func main() {
text, err := handOver(3)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(text)
}
What to look for
Step into handOver and then into findCoat. tag is 3, so the next stop after if tag == 1 is the return with errors.New. Back in handOver, err shows error(*errors.errorString), an address, → and {s: "no coat for this tag"}: first error, then in parentheses the type of the value inside the error (errors.New makes an *errors.errorString, a pointer), then the address in that pointer, and after → the value it points to, where s holds the message. The stepper goes from if err != nil to return "", err, and the line return "here is your " + coat, nil never runs. main does the same with its own if, and the run prints error: no coat for this tag. After the edit, err shows error nil in handOver and in main, each if is skipped, and the run prints here is your blue scarf.
Expected output
error: no coat for this tag02 / Lost errors
How can an error get lost?
An error is an ordinary value, so the compiler lets you throw it away. Writing _ in place of err discards the error: coat, _ := findCoat(9) gives coat the empty string, and the program continues as if the call had worked. A call written on its own line, findCoat(9), discards both results the same way. Write _ for an error only when the function’s documentation says the error cannot happen, and say why in a comment.
The second way gives no warning at all. Variables showed shadowing: := in an inner block declares a new variable that hides the outer one. A loop body and an if block are inner blocks. So coat, err := findCoat(tag) inside a loop declares a new err, and the error is stored there. The err outside the loop never changes, and the function returns it, still nil. In the same block, := would reuse the existing err, as long as at least one name on its left is new.
The third way keeps the error but loses which error it was: adding context with %v where %w was needed. The wrapping section shows it.
func pickUp(tags []int) (coat string, err error) {
for _, tag := range tags {
// declares a new coat and err for the loop body
coat, err := findCoat(tag)
if err == nil {
return coat, nil
}
}
// the result err, never changed
return coat, err
} An error is lost when code discards it with _, stores it with := in a new err inside an inner block, or wraps it with %v where %w was needed.
Coming from Python: An exception that nothing catches still stops the program with a message; a Go error that nothing checks disappears without any message.
Precisely: tools that find these
The compiler rejects a local variable that is declared and never used, but not a discarded result. go vet does not report either case with its default checks. The errcheck tool reports discarded errors, and the shadow analyzer, which is not part of the default go vet checks, reports a variable that hides another with the same name. pickUp names its results, (coat string, err error): named results are variables declared at the top of the function, and a bare return statement returns them. A bare return inside the loop would not compile: the compiler reports result parameter coat not in scope at return.
Experiment
Lose an error to := inside a loop
The results (coat string, err error) are variables declared at the top of pickUp. Inside the loop body, := declares a new coat and a new err, because the loop body is an inner block. The error is stored in the new err, which ends with each pass of the loop. The return after the loop reads the result err, which nothing changed.
Why it matters Inside an inner block, read every := with err on the left and ask which err it declares. Use = when the error must reach a variable outside the block.
pickUp tries tags 7 and 9, and neither has a coat. Predict what main prints. Then change coat, err := findCoat(tag) to coat, err = findCoat(tag) and predict again.
package main
import (
"errors"
"fmt"
)
func findCoat(tag int) (string, error) {
if tag == 1 {
return "blue scarf", nil
}
return "", errors.New("no coat for this tag")
}
func pickUp(tags []int) (coat string, err error) {
for _, tag := range tags {
coat, err := findCoat(tag)
if err == nil {
return coat, nil
}
}
return coat, err
}
func main() {
coat, err := pickUp([]int{7, 9})
fmt.Printf("coat %q, error %v\n", coat, err)
}
What to look for
Stop on if err == nil inside the loop and look at pickUp’s frame. It lists (coat) and (err) in parentheses: these are the result variables, hidden by the new coat and err that := declared. The new err shows the “no coat for this tag” error, while (err) still shows error nil. After the loop, the parentheses are gone and err shows error nil, so the run prints coat "", error <nil>: a success with no coat. After the edit, the frame has one err. It holds the error after each call, pickUp returns it, and the run prints coat "", error no coat for this tag. At the first stop in pickUp, before the function sets its results, coat can show leftover bytes and err can show “unreadable”.
Expected output
coat "", error <nil>03 / Sentinels
How does the calling code tell one error from another?
The code that receives an error often has to react to one failure differently from another. A missing coat means “ask the guest to check the tag”; a closed cloakroom means “come back later”. The simplest way to tell them apart is a sentinel error: an error value made once, stored in a package-level variable, and returned by every function that reports that failure.
By convention, the variable’s name starts with Err; a capital first letter also makes it visible to other packages. The standard library has many sentinels: io.EOF is the error Read returns when there is nothing more to read.
The comparison checks whether it is the same value, not the same text. Each call to errors.New makes a new error, so errors.New("x") == errors.New("x") is false. That is why a sentinel is made once and stored.
var ErrNoCoat = errors.New("no coat for this tag")
func findCoat(tag int) (string, error) {
if tag == 1 {
return "blue scarf", nil
}
return "", ErrNoCoat
}
_, err := findCoat(9)
fmt.Println(err == ErrNoCoat) // true A sentinel error is one value made once; the calling code recognizes it by comparing values, not messages.
Coming from Rust: A Rust Result<T, E> names its error type, and match must cover every variant; a Go signature says only error, so the documentation has to list the sentinels a function can return.
Precisely: what == compares
errors.New returns a pointer to a new struct. Comparing two interface values compares the types of the values inside them, then the values, which here are pointers. So two sentinels with the same text are still different errors. This == stops matching as soon as anything wraps the error, which the next section shows.
04 / Wrapping
How do you add context without losing which error it was?
The message no coat for this tag does not say which tag or which step failed. Each function that passes an error on can add that context. fmt.Errorf builds a new error from a format, like fmt.Sprintf. With the %w verb, the new error also keeps the original error inside it. This is wrapping. Each wrap adds a layer, and the errors inside each other form a chain. The message lists the layers from the outermost one: pick up tag 9: no coat for this tag.
After wrapping, err == ErrNoCoat is false, because == compares only the outer error. errors.Is(err, ErrNoCoat) searches the chain: it compares err, then the error inside it, and so on, and returns true if any of them matches. Use errors.Is whenever an error may have been wrapped.
The %v verb only copies the original message into the new text. The message looks exactly the same, but the original error is not inside it any more, so errors.Is returns false.
So the choice between the two verbs is a choice about the code that called you. Use %w when that code may need to check the cause. Use %v when the cause is an internal detail that the calling code should not depend on, such as which storage library failed.
func pickUp(tag int) (string, error) {
coat, err := findCoat(tag)
if err != nil {
return "", fmt.Errorf("pick up tag %d: %w", tag, err)
}
return coat, nil
}
_, err := pickUp(9)
fmt.Println(err) // pick up tag 9: no coat for this tag
fmt.Println(errors.Is(err, ErrNoCoat)) // true Wrap with %w when the calling code may check the cause, and with %v when the cause is internal; check with errors.Is, which searches every layer.
Coming from Java: fmt.Errorf with %w does the job of new RuntimeException("pick up", cause), and errors.Is searches the causes; Go writes the context into the message itself.
Precisely: Unwrap and several errors
The error that fmt.Errorf returns for %w has an Unwrap method that returns the error inside it, and errors.Unwrap(err) calls it once. Since Go 1.20, one fmt.Errorf may use %w more than once, and errors.Join combines several errors; errors.Is then searches each of them. A type may also define its own Is method. The Go blog on errors in Go 1.13 explains the choice between %w and %v: wrapping makes the inner error part of what your function promises.
Experiment
Wrap an error and compare it two ways
With %w, fmt.Errorf returns a new error that keeps the original inside it. == compares only the outer error, which is not ErrNoCoat. errors.Is looks at the outer error, then the one inside it, and finds ErrNoCoat. With %v, fmt.Errorf copies the original message into new text and keeps nothing else.
Why it matters A message that reads the same does not mean the same error is inside. Check with errors.Is, and wrap with %w when the code that called you may need to check.
Predict both output lines. Then change %w to %v in pickUp and predict again.
package main
import (
"errors"
"fmt"
)
var ErrNoCoat = errors.New("no coat for this tag")
func findCoat(tag int) (string, error) {
if tag == 1 {
return "blue scarf", nil
}
return "", ErrNoCoat
}
func pickUp(tag int) (string, error) {
coat, err := findCoat(tag)
if err != nil {
return "", fmt.Errorf("pick up tag %d: %w", tag, err)
}
return coat, nil
}
func main() {
_, err := pickUp(9)
fmt.Println(err)
fmt.Println(err == ErrNoCoat, errors.Is(err, ErrNoCoat))
}
What to look for
Step until pickUp returns, then look at err in main. Its type is *fmt.wrapError: msg holds the whole message, and err holds the original *errors.errorString inside it. That is the chain, one layer deep. The run prints pick up tag 9: no coat for this tag, then false true. After the edit, err shows *errors.errorString with the whole message in s and no error inside it. The first line is the same, and the second becomes false false.
Expected output
pick up tag 9: no coat for this tag
false true05 / Error types
How do you carry details in an error?
A sentinel says which failure happened, but it cannot carry data, such as which tag had no coat. For that, define an error type: a struct with fields for the details, and an Error() string method. (e *TagError) before the method name means the method is called on a *TagError, and e is that pointer. Because *TagError has the method, a *TagError can be returned as an error.
The calling code receives an error, not a *TagError, so err.Tag does not compile. The compiler reports err.Tag undefined (type error has no field or method Tag). errors.As searches the chain like errors.Is, but it looks for an error of a type you choose. You pass a pointer to a variable of that type. When errors.As finds a match, it stores the error in the variable and returns true.
One trap belongs to error types. An error value holds two things: the type of the value inside it and the value. It is nil only when both are empty. A nil *TagError returned as an error still has a type, so err != nil is true. Return nil itself on success.
type TagError struct {
Tag int
}
func (e *TagError) Error() string {
return fmt.Sprintf("no coat for tag %d", e.Tag)
}
var tagErr *TagError
if errors.As(err, &tagErr) {
fmt.Println("check tag", tagErr.Tag)
} An error type carries details, and errors.As finds it through any wrapping.
Coming from Java: A Java catch (TagException e) matches however the exception was created. errors.As matches the exact type inside the error: var te *TagError; errors.As(err, &te) matches only an error created as &TagError{…}. If Error had a value receiver, func (e TagError), an error created as TagError{…} would need var te TagError. With the pointer receiver used here, var te TagError makes errors.As panic, because TagError itself has no Error method.
Precisely: what errors.As accepts
The second argument must be a non-nil pointer to a type that implements error, or to an interface type; otherwise errors.As panics. Because Error has a pointer receiver here, the error type is *TagError, so the variable is a *TagError and you pass &tagErr. Go 1.26 added errors.AsType, a generic form: tagErr, ok := errors.AsType[*TagError](err).
Experiment
Return a nil pointer as an error
An error value holds two things: the type of the value inside it and the value itself. It equals nil only when both are empty. Returning problem stores the type *TagError together with a nil pointer, so err != nil is true although the pointer is nil.
Why it matters Return nil itself on success, not a nil pointer of your error type. Declare a variable that may hold an error as error, not as a pointer to your type.
check(1) never sets problem. Predict what main prints. Then change var problem *TagError to var problem error and predict again.
package main
import "fmt"
type TagError struct {
Tag int
}
func (e *TagError) Error() string {
return fmt.Sprintf("no coat for tag %d", e.Tag)
}
func check(tag int) error {
var problem *TagError
if tag > 2 {
problem = &TagError{Tag: tag}
}
return problem
}
func main() {
err := check(1)
if err != nil {
fmt.Println("failed:", err)
return
}
fmt.Println("tag 1 is fine")
}
What to look for
In check, problem shows *main.TagError nil. Back in main, err shows error(*main.TagError) nil: the error holds a type and a nil pointer, so it is not nil, and the if is true. The stepper then enters (*TagError).Error with e showing nil. Reading e.Tag through the nil pointer fails, fmt catches that failure and prints <nil> instead, and the run prints failed: <nil>. After the edit, problem and err show error nil, the if is false, and the run prints tag 1 is fine.
Expected output
failed: <nil>06 / In production
What goes wrong with errors in real programs?
Each function in a real program may check its errors and still lose information. Three mistakes are common.
Handle an error once. A function that writes an error to the log and also returns it puts the same failure in the log again for each function that does the same. Each function should do one of the two: log the error and stop passing it on, or wrap the error and return it.
Keep the error from a deferred Close. defer f.Close() discards the error that Close returns. On a file you wrote to, Close can be the only call that reports that the data was not saved, for example on a network file system, or on a gzip.Writer, which writes its last bytes in Close. Name the error result, and let a deferred function set it when nothing failed earlier, as below. errors.Join(err, cerr) is another choice, when both errors should be kept.
Check context errors with errors.Is. Work that is canceled, or runs out of time, through a context.Context returns context.Canceled or context.DeadlineExceeded. Each function on the way usually wraps that error, so err == context.Canceled is false where errors.Is(err, context.Canceled) is true. A canceled request is usually not a failure to report.
func save(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
_, err = f.Write(data)
return err
} Handle each error once, keep the error from a deferred Close, and check context errors with errors.Is.
Precisely: := next to a named result
f, err := os.Create(path) does not declare a new err. At the top level of a function body, := reuses a parameter or result of the same name, as the specification allows, when at least one name on its left, here f, is new. Inside an if or a loop, the same line would declare a new err, as the section on lost errors showed.
Experiment
Return an error from a deferred Close
In a function with named results, a return statement first sets the results, then runs the deferred calls, and then the function ends. The deferred function can read and set err, because err is a variable of record. defer l.Close() calls Close at the same moment, but nothing receives the result of Close.
Why it matters When Close can fail, as on a file you wrote to, do not write defer f.Close() alone. Name the error result, and set it from a deferred function when no earlier error is set.
Write succeeds and Close fails. Predict what main prints. Then replace the whole defer func() { … }() statement with defer l.Close() and predict again.
package main
import (
"errors"
"fmt"
)
// Ledger stands in for a file: Close saves the lines.
type Ledger struct {
lines []string
}
func (l *Ledger) Write(line string) error {
l.lines = append(l.lines, line)
return nil
}
func (l *Ledger) Close() error {
return errors.New("disk full")
}
func record(l *Ledger, tag int) (err error) {
defer func() {
if cerr := l.Close(); cerr != nil && err == nil {
err = cerr
}
}()
return l.Write(fmt.Sprintf("tag %d", tag))
}
func main() {
err := record(&Ledger{}, 4)
fmt.Println("record:", err)
}
What to look for
Step past return l.Write(…) in record. The return sets err to error nil, and the next stop is not in main: the stepper enters the deferred function, which it calls record.func1 because the function has no name. There, err shows error nil, Close returns the “disk full” error in cerr, and err = cerr changes the result. record’s own err changes too, because the deferred function uses the same variable. Back in main, err shows the error, and the run prints record: disk full. After the edit, the stepper goes from Close back to main, err shows error nil, and the run prints record: <nil>, although the lines were not saved. The frame list then shows record.deferwrap1, a wrapper the compiler makes for the deferred call.
Expected output
record: disk full07 / Try it
Can you predict which checks match?
Follow each error through pickUp, then decide what each check sees. The cloakroom is open in the first call and closed in the second.
package main
import (
"errors"
"fmt"
)
var ErrClosed = errors.New("cloakroom closed")
type TagError struct {
Tag int
}
func (e *TagError) Error() string {
return fmt.Sprintf("no coat for tag %d", e.Tag)
}
func findCoat(tag int, open bool) (string, error) {
if !open {
return "", ErrClosed
}
if tag != 1 {
return "", &TagError{Tag: tag}
}
return "blue scarf", nil
}
func pickUp(tag int, open bool) (string, error) {
coat, err := findCoat(tag, open)
if err != nil {
return "", fmt.Errorf("pick up: %w", err)
}
return coat, nil
}
func main() {
_, err := pickUp(4, true)
var tagErr *TagError
fmt.Println(err)
fmt.Println(errors.Is(err, ErrClosed), errors.As(err, &tagErr))
_, err = pickUp(1, false)
fmt.Println(err == ErrClosed, errors.Is(err, ErrClosed))
}
What are the three output lines?
Reveal the reasoning
pick up: no coat for tag 4, then false true, then false true. The first error is a *TagError wrapped once: it is not ErrClosed, and errors.As finds the *TagError inside and stores it in tagErr, whose Tag is 4. The second error is ErrClosed wrapped once, so == compares the outer error and gives false, while errors.Is finds ErrClosed inside and gives true.
Practice tickets
- GO-201 · A refused late checkout shows no reason → Free
- GO-202 · One full ferry ends the whole search → Sign in · Pro
- GO-203 · Readings disappear but the upload reports success → Sign in · Pro
Keep exploring
I/O, buffering, and external state →