Skip to main content

Overview

In Go, it’s idiomatic to communicate errors via an explicit, separate return value. This contrasts with the exceptions used in languages like Java, Python, and Ruby. Go’s approach makes it easy to see which functions return errors and to handle them using the same language constructs employed for other tasks.
See the errors package documentation and this blog post for additional details.

Basic Error Handling

By convention, errors are the last return value and have type error, a built-in interface.
func f(arg int) (int, error) {
    if arg == 42 {
        // errors.New constructs a basic error value
        // with the given error message
        return -1, errors.New("can't work with 42")
    }

    // A nil value in the error position indicates that
    // there was no error
    return arg + 3, nil
}

Inline Error Checking

It’s idiomatic to use an inline error check in the if line:
for _, i := range []int{7, 42} {
    if r, e := f(i); e != nil {
        fmt.Println("f failed:", e)
    } else {
        fmt.Println("f worked:", r)
    }
}

Sentinel Errors

A sentinel error is a predeclared variable that is used to signify a specific error condition:
var ErrOutOfTea = errors.New("no more tea available")
var ErrPower = errors.New("can't boil water")

func makeTea(arg int) error {
    if arg == 2 {
        return ErrOutOfTea
    } else if arg == 4 {
        return ErrPower
    }
    return nil
}

Error Wrapping

You can wrap errors with higher-level errors to add context. The simplest way to do this is with the %w verb in fmt.Errorf:
func makeTea(arg int) error {
    if arg == 4 {
        // Wrapped errors create a logical chain (A wraps B, which wraps C, etc.)
        // that can be queried with functions like errors.Is and errors.AsType
        return fmt.Errorf("making tea: %w", ErrPower)
    }
    return nil
}
Wrapped errors create a logical chain that can be queried with functions like errors.Is and errors.AsType.

Error Inspection

errors.Is

errors.Is checks that a given error (or any error in its chain) matches a specific error value. This is especially useful with wrapped or nested errors:
for i := range 5 {
    if err := makeTea(i); err != nil {
        if errors.Is(err, ErrOutOfTea) {
            fmt.Println("We should buy new tea!")
        } else if errors.Is(err, ErrPower) {
            fmt.Println("Now it is dark.")
        } else {
            fmt.Printf("unknown error: %s\n", err)
        }
        continue
    }
    fmt.Println("Tea is ready!")
}

Complete Example

package main

import (
	"errors"
	"fmt"
)

func f(arg int) (int, error) {
	if arg == 42 {
		return -1, errors.New("can't work with 42")
	}
	return arg + 3, nil
}

var ErrOutOfTea = errors.New("no more tea available")
var ErrPower = errors.New("can't boil water")

func makeTea(arg int) error {
	if arg == 2 {
		return ErrOutOfTea
	} else if arg == 4 {
		return fmt.Errorf("making tea: %w", ErrPower)
	}
	return nil
}

func main() {
	for _, i := range []int{7, 42} {
		if r, e := f(i); e != nil {
			fmt.Println("f failed:", e)
		} else {
			fmt.Println("f worked:", r)
		}
	}

	for i := range 5 {
		if err := makeTea(i); err != nil {
			if errors.Is(err, ErrOutOfTea) {
				fmt.Println("We should buy new tea!")
			} else if errors.Is(err, ErrPower) {
				fmt.Println("Now it is dark.")
			} else {
				fmt.Printf("unknown error: %s\n", err)
			}
			continue
		}
		fmt.Println("Tea is ready!")
	}
}

Output

$ go run errors.go
f worked: 10
f failed: can't work with 42
Tea is ready!
Tea is ready!
We should buy new tea!
Tea is ready!
Now it is dark.

Best Practices

Always check errors

Don’t ignore error return values. Check them immediately after function calls.

Use sentinel errors

Define package-level error variables for common error conditions.

Add context

Wrap errors with additional context using fmt.Errorf and %w.

Use errors.Is

Check for specific errors in wrapped error chains with errors.Is.

Build docs developers (and LLMs) love