Error handling in Go

Learn idiomatic error handling in Go: the error interface, returning and checking errors, errors.New, wrapping with %w, errors.Is and errors.As.

Prerequisites

Learning objectives

  • Return and check errors with the error interface and if err != nil
  • Create errors with errors.New and add context by wrapping with %w
  • Match errors using sentinels, errors.Is, and errors.As

Go has no exceptions. A function that can fail returns an error as its last result, and the caller checks it. This makes the failure path explicit and visible right where it happens.

The error interface

error is just a built-in interface with a single method. Any type with an Error() string method is an error.

type error interface {
	Error() string
}

A nil error means success; a non-nil error describes what went wrong.

Returning and checking errors

The idiomatic pattern is to return the error last and check it immediately with if err != nil. You either handle it or return it to your own caller.

f, err := os.Open("config.txt")
if err != nil {
	return err // handle it here, or propagate it
}
defer f.Close()

Creating errors with errors.New

For a simple, fixed message, use errors.New. For a message that includes values, use fmt.Errorf.

err := errors.New("cannot divide by zero")
err = fmt.Errorf("user %d is not allowed", id)

Wrapping errors with %w

When you propagate an error, add context so the final message reads like a trail. The %w verb in fmt.Errorf wraps the original error, keeping it reachable for later inspection.

if err != nil {
	return fmt.Errorf("load config: %w", err)
}

Sentinel errors and errors.Is

A sentinel is a predefined error value that callers compare against. Because %w preserves the wrapped error, use errors.Is (not ==) so the match still works through layers of wrapping.

var ErrNotFound = errors.New("not found")

if errors.Is(err, ErrNotFound) {
	// handle the not-found case
}

Custom error types and errors.As

When you need to attach data to an error, define a type with an Error method. errors.As unwraps the chain and, if it finds a matching type, fills in your variable so you can read its fields.

type ValidationError struct {
	Field string
}

func (e *ValidationError) Error() string {
	return "invalid field: " + e.Field
}

var vErr *ValidationError
if errors.As(err, &vErr) {
	fmt.Println("bad field:", vErr.Field)
}

A complete example

The program below looks up a user, wraps a sentinel error with context, and uses errors.Is to react to the not-found case. It compiles and runs as-is.

package main

import (
	"errors"
	"fmt"
)

// ErrNotFound is a sentinel error callers can compare against.
var ErrNotFound = errors.New("user not found")

var users = map[int]string{
	1: "Ada",
	2: "Alan",
}

// findUser returns the name, or wraps ErrNotFound with context.
func findUser(id int) (string, error) {
	name, ok := users[id]
	if !ok {
		return "", fmt.Errorf("findUser %d: %w", id, ErrNotFound)
	}
	return name, nil
}

func main() {
	for _, id := range []int{1, 42} {
		name, err := findUser(id)
		if err != nil {
			if errors.Is(err, ErrNotFound) {
				fmt.Printf("id %d: not found\n", id)
				continue
			}
			fmt.Printf("id %d: unexpected error: %v\n", id, err)
			continue
		}
		fmt.Printf("id %d: %s\n", id, name)
	}
}

Running it prints:

id 1: Ada
id 42: not found

Cleanup with defer, and panic as a last resort

defer schedules a call to run when the surrounding function returns, whatever path it takes. It is the standard way to release resources such as files or locks, so cleanup sits right next to the acquisition.

f, err := os.Open("data.txt")
if err != nil {
	return err
}
defer f.Close() // always runs on the way out

Common mistakes

  • Ignoring errors. Writing f, _ := os.Open(...) hides failures. Handle or return every error you get.
  • Wrapping with %v instead of %w. %v formats the message but discards the original error, so errors.Is and errors.As can no longer see it.
  • Comparing wrapped errors with ==. Once an error is wrapped, use errors.Is to match a sentinel.
  • Panicking on ordinary failures. A missing file or bad input is expected; return an error rather than crashing the program.

Practice

Write a divide(a, b int) (int, error) that returns an error created with errors.New when b is zero, then call it with a zero divisor and print the error.

Show the solution
package main

import (
	"errors"
	"fmt"
)

func divide(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("cannot divide by zero")
	}
	return a / b, nil
}

func main() {
	result, err := divide(10, 0)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("result:", result)
}

divide returns the error as its last value; main checks it with if err != nil before touching result. With a zero divisor the guard fires and the program prints error: cannot divide by zero.

Summary

  • error is an interface; a nil error means success.
  • Return errors last and check them with if err != nil.
  • Create errors with errors.New/fmt.Errorf, and wrap with %w to keep the original reachable.
  • Match errors with errors.Is (sentinels) and errors.As (custom types).

Next, revisit the interfaces in Go that error itself is built on. To see where errors first surface as return values, review functions in Go.

Your progress is stored only in this browser. No account is required.

Search

Search runs entirely in your browser.