Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

8. Errors

Go has no exceptions. A function that can fail says so in its signature by returning an error alongside its result, the caller checks it on the next line, and control flow stays where you can see it. This is the part of Go that people complain about most and change their minds about latest.

8.1 Error Is an Interface

type error interface {
	Error() string
}

That is the definition, copied from the standard library. Any type with an Error() string method is an error, which means errors are ordinary values: you can store them, compare them, put them in a slice, return them from a closure, and define your own.

The convention is that error is the last result and a nil error means success. Every function in the standard library follows it, and so should yours.

f, err := os.Open(path)
if err != nil {
	return err
}
defer f.Close()

Three lines per call is a real cost and Go pays it deliberately. The trade is that a reader can see every place a function can fail without knowing which calls throw and which do not, and there is no invisible jump out of the middle of a function. Whether that is worth it is an argument you can have; how to write it well is not, and the rest of this chapter is that.

8.2 Making Errors

errors.New builds one from a string. fmt.Errorf formats one, and its %w verb wraps another error inside the new one.

// A sentinel error, a wrapped error, and the two ways to inspect one.
package main

import (
	"errors"
	"fmt"
	"strconv"
)

// ErrNegative is returned for an age below zero. Callers compare against it
// with errors.Is rather than by string.
var ErrNegative = errors.New("age must not be negative")

func parseAge(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, fmt.Errorf("parsing age %q: %w", s, err)
	}
	if n < 0 {
		return 0, fmt.Errorf("parsing age %q: %w", s, ErrNegative)
	}
	return n, nil
}

func main() {
	for _, in := range []string{"42", "-1", "old"} {
		n, err := parseAge(in)
		switch {
		case err == nil:
			fmt.Println("ok:", n)
		case errors.Is(err, ErrNegative):
			fmt.Println("negative:", err)
		default:
			var numErr *strconv.NumError
			if errors.As(err, &numErr) {
				fmt.Printf("not a number: %v (input %q)\n", err, numErr.Num)
			}
		}
	}
}
ok: 42
negative: parsing age "-1": age must not be negative
not a number: parsing age "old": strconv.Atoi: parsing "old": invalid syntax (input "old")

Add context as the error travels up. Each layer prefixes what it was doing, so parsing age "old": strconv.Atoi: parsing "old": invalid syntax tells you the whole path without a stack trace. Write the message lower-case and without punctuation at the end, because it will be embedded in someone else’s sentence. And do not include the word “error” or “failed”; the reader knows.

Use %w when the caller might reasonably want to inspect the underlying error and %v when it is an implementation detail you would rather not promise. %w makes the wrapped error part of your API.

8.3 Sentinels and Types

Two ways to let a caller ask what went wrong, and one way that does not work.

A sentinel is a package-level error value that callers compare against, like ErrNegative above or io.EOF, os.ErrNotExist, and sql.ErrNoRows in the standard library. Compare with errors.Is, never with ==, because == fails the moment somebody wraps it. errors.Is unwraps as it goes.

A custom type carries structured detail that a message cannot.

// An error type carrying structured detail.
package main

import (
	"errors"
	"fmt"
	"strings"
)

type ParseError struct {
	Line int
	Text string
}

func (e *ParseError) Error() string {
	return fmt.Sprintf("line %d: cannot parse %q", e.Line, e.Text)
}

func parse(input string) (map[string]string, error) {
	out := map[string]string{}
	for i, line := range strings.Split(input, "\n") {
		if line == "" {
			continue
		}
		k, v, ok := strings.Cut(line, "=")
		if !ok {
			return nil, fmt.Errorf("config: %w", &ParseError{Line: i + 1, Text: line})
		}
		out[k] = v
	}
	return out, nil
}

func main() {
	_, err := parse("a=1\nbroken\nc=3")
	fmt.Println(err)

	var pe *ParseError
	if errors.As(err, &pe) {
		fmt.Println("the bad line number was", pe.Line)
	}
}
config: line 2: cannot parse "broken"
the bad line number was 2

errors.As looks down the chain for an error of the type you asked for and assigns it to your variable, so the caller can reach pe.Line. Note the pointer receiver on Error(), which means *ParseError is the error type and ParseError is not, and that errors.As takes &pe, a pointer to the pointer.

The way that does not work is strings.Contains(err.Error(), "not found"). Error strings are for humans and change without notice.

When several things are wrong at once, errors.Join combines them into one error whose message lists them and which errors.Is can still search:

// Reporting every problem instead of only the first.
package main

import (
	"errors"
	"fmt"
)

type User struct {
	Name  string
	Email string
	Age   int
}

func validate(u User) error {
	var errs []error
	if u.Name == "" {
		errs = append(errs, errors.New("name is empty"))
	}
	if u.Email == "" {
		errs = append(errs, errors.New("email is empty"))
	}
	if u.Age < 0 {
		errs = append(errs, errors.New("age is negative"))
	}
	return errors.Join(errs...) // nil when errs is empty
}

func main() {
	fmt.Println(validate(User{Name: "Ada", Email: "ada@example.com", Age: 36}))
	if err := validate(User{Age: -1}); err != nil {
		fmt.Println("invalid:")
		fmt.Println(err)
	}
}
<nil>
invalid:
name is empty
email is empty
age is negative

errors.Join returns nil when every argument is nil, so the accumulate-then-join shape works without a length check.

8.4 The Typed Nil

This is Go’s most famous bug and it is worth ten minutes of your attention now rather than an afternoon later.

// A nil pointer inside a non-nil interface. Go's most famous error bug.
package main

import "fmt"

type ParseError struct{ Line int }

func (e *ParseError) Error() string { return fmt.Sprintf("line %d", e.Line) }

// broken returns a typed nil, which is not a nil error.
func broken() error {
	var e *ParseError // nil
	return e          // wrapped in an interface: not nil
}

// fixed returns the untyped nil.
func fixed() error {
	return nil
}

func main() {
	fmt.Println(broken() == nil) // false
	fmt.Println(fixed() == nil)  // true

	if err := broken(); err != nil {
		fmt.Printf("caller thinks it failed: %v (%T)\n", err, err)
	}
}
false
true
caller thinks it failed: <nil> (*main.ParseError)

An interface value holds two things, a type and a value, and it is nil only when both are. broken returns a *ParseError that is nil, but the interface it gets stored in records the type *ParseError, so the interface is not nil and err != nil is true. The caller reports a failure that did not happen.

Avoid it with one rule: declare error variables as error, not as a concrete pointer type. If broken had written var e error and assigned to it only on the failing path, there would be nothing to go wrong. The bug arrives when somebody declares var e *ParseError at the top of a function so they can fill in fields, then returns it whether or not it was ever used.

8.5 Panic

panic is for a bug in your program, not for a condition your program should handle. An index out of range, a nil dereference, an impossible switch default: these mean the code is wrong, and continuing would do damage. Everything else — a missing file, bad input, a refused connection — is an error value, because it is a thing that happens to a correct program.

The test to apply: could a caller reasonably recover from this? If yes, it is an error. A library that panics on bad input has taken that choice away from everyone who imports it.

recover belongs at a boundary you control, and there are two of them worth the trouble. A recursive parser can panic its way out of a malformed input and recover at the top-level entry point, turning the panic back into an error before it escapes the package. And a server can recover per request, so that one bad handler loses one response rather than every connection in flight.

8.6 Errors at the Top

main is where errors stop. The pattern is to keep main short and put the work in a function that returns an error:

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "myprog:", err)
		os.Exit(1)
	}
}

Diagnostics go to standard error, prefixed with the program name, so a user piping your output gets data on one stream and complaints on the other. os.Exit with a non-zero status is how the shell finds out. Note that os.Exit does not run deferred functions, the reason to call it here rather than deep in the program. Chapter 10 builds a program around this shape.

Exercise 8-1. Write func readConfig(path string) (map[string]string, error) that wraps both the file error and any parse error with the path.

Exercise 8-2. Give ParseError a Column field and an Unwrap method returning an underlying cause, then check that errors.Is finds a sentinel through two layers.

Exercise 8-3. Write a function returning (*bytes.Buffer, error) that demonstrates the typed-nil bug, then fix it two ways: by changing the declaration, and by returning an explicit nil on the success path.

Exercise 8-4. Read the io.EOF documentation and explain why errors.Is(err, io.EOF) and err == io.EOF differ for a wrapped reader.