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.