Go path · Values and types · Topic 5 of 13

Errors are values

Not started

In Go a failure is a value. error is an ordinary interface with one method, Error() string, and a function that can fail returns one as its last result. There is no hidden control flow: the caller sees if err != nil and decides what to do. The standard library adds a convention on top: wrap an error to add context, and inspect the chain with errors.Is and errors.As rather than by matching strings.

Why it matters: Go has no exceptions for ordinary failures. Good error handling is ordinary code: add context on the way up, and let callers check what went wrong without parsing strings.

  1. 1Just an interface

    error is an interface with one method, Error() string. Functions return it as their last result and callers check it.

  2. 2Wrap with %w

    fmt.Errorf with %w adds context and keeps the original error inside, reachable through Unwrap.

  3. 3Is and As look through wrappers

    errors.Is compares against a sentinel value; errors.As finds an error of a given type. Both walk the whole chain.

  4. 4panic is for bugs

    panic unwinds the stack and crashes the program unless recovered. Use it for impossible states, not for a missing file.

1

How it works

The explanation, with examples you can run.

Wrapping and inspecting

package main import (	"errors"	"fmt"	"io/fs"	"os") var ErrNoConfig = errors.New("no config") func loadConfig(path string) error {	_, err := os.ReadFile(path)	if errors.Is(err, fs.ErrNotExist) {		return fmt.Errorf("load %s: %w", path, ErrNoConfig)	}	if err != nil {		return fmt.Errorf("load %s: %w", path, err)	}	return nil} func main() {	err := loadConfig("/nope/app.toml")	fmt.Println(err)	fmt.Println(errors.Is(err, ErrNoConfig)) 	_, err = os.Open("/nope/app.toml")	err = fmt.Errorf("startup: %w", err)	var pe *fs.PathError	if errors.As(err, &pe) {		fmt.Println("op:", pe.Op, "path:", pe.Path)	}	if pe, ok := errors.AsType[*fs.PathError](err); ok { // Go 1.26+		fmt.Println("same with AsType:", pe.Err)	}}

This prints load /nope/app.toml: no config, true, op: open path: /nope/app.toml, and same with AsType: no such file or directory.

  • Sentinel errors such as ErrNoConfig or io.EOF are package-level values. Check them with errors.Is, which follows Unwrap through every layer.
  • Error types such as *fs.PathError carry fields. errors.As finds the first error in the chain that has that type and stores it in your variable. Go 1.26 added errors.AsType, a generic version that returns the value directly and is checked at compile time.
  • %w makes the result unwrappable; %v only copies the text. Wrapping exposes the inner error as part of your API, so wrap when callers should be able to depend on it.
  • errors.Join (Go 1.20) combines several errors into one; Is and As check each of them.

Adding context

Each layer should say what it was doing: fmt.Errorf("charge order %s: %w", id, err). Read from the top, the message becomes a trail such as charge order 42: call payments: context deadline exceeded. Don't log and return the same error: it gets reported twice. Handle it once, at the level that can do something about it.

panic and recover

panic stops the normal flow, runs deferred calls as it unwinds the goroutine's stack, and crashes the program with a stack trace if nothing calls recover. recover only works inside a deferred function. Reserve panics for bugs (an index out of range, a nil map write, an impossible case) and for failures during start-up. net/http recovers a panic in a handler and logs it, but a panic in a goroutine you started yourself takes down the whole process. A library shouldn't let a panic escape its API.

Deferred calls also run on a normal return, which is why defer f.Close() right after a successful Open is the usual way to release resources.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.err is fmt.Errorf("load: %w", fs.ErrNotExist). Which check is true?
  2. 2.What is the difference between %v and %w in fmt.Errorf?
  3. 3.Which situation calls for panic rather than returning an error?

0 of 3 right so far. Get all three to finish this topic.