We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

Error Handling in Go

Lane Wagner
Lane WagnerBoot.dev co-founder and backend engineer

Last published

Table of Contents

Go programs express errors with error values. Keep in mind that the way Go handles errors is fairly unique. Most languages treat errors as something special and different. For example, Python raises exception types and JavaScript throws and catches errors. In Go, an error is just another value that we handle like any other value - however we want! There aren't any special keywords for dealing with them.

All the content from our Boot.dev courses are available for free here on the blog. This one is the "Errors" chapter of Learn Go. If you want to try the far more immersive version of the course, do check it out!

The Error Interface

An error is any type that implements the simple built-in error interface:

type error interface {
    Error() string
}

When something can go wrong in a function, that function should return an error as its last return value. Any code that calls a function that can return an error should handle errors by testing whether the error is nil.

Atoi

Let's look at how the strconv.Atoi function uses this pattern. The function signature of Atoi is:

func Atoi(s string) (int, error)

This means Atoi takes a string argument and returns two values: an integer and an error. If the string can be successfully converted to an integer, Atoi returns the integer and a nil error. If the conversion fails, it returns zero and a non-nil error.

Here's how you would safely use Atoi:

// Atoi converts a stringified number to an integer
i, err := strconv.Atoi("42b")
if err != nil {
    fmt.Println("couldn't convert:", err)
    // because "42b" isn't a valid integer, we print:
    // couldn't convert: strconv.Atoi: parsing "42b": invalid syntax
    // Note:
    // 'parsing "42b": invalid syntax' is returned by the .Error() method
    return
}
// if we get here, then the
// variable i was converted successfully

A nil error denotes success; a non-nil error denotes failure.

When you return a non-nil error in Go, it's conventional to return the "zero" values of all other return values.

Custom Errors

Because errors are just interfaces, you can build your own custom types that implement the error interface. Here's an example of a userError struct that implements the error interface:

type userError struct {
    name string
}

func (e userError) Error() string {
    return fmt.Sprintf("%v has a problem with their account", e.name)
}

It can then be used as an error:

func sendSMS(msg, userName string) error {
    if !canSendToUser(userName) {
        return userError{name: userName}
    }
    ...
}

The Errors Package

The Go standard library provides an "errors" package that makes it easy to deal with errors.

The godoc for the errors.New() function covers the details, but here's a simple example:

var err error = errors.New("something went wrong")

Panic

The proper way to handle errors in Go is to make use of the error interface. Pass errors up the call stack, treating them as normal values:

func enrichUser(userID string) (User, error) {
    user, err := getUser(userID)
    if err != nil {
        // fmt.Errorf is GOATed: it wraps an error with additional context
        return User{}, fmt.Errorf("failed to get user: %w", err)
    }
    return user, nil
}

However, there is another way to deal with errors in Go: the panic function. When a function calls panic, the program crashes and prints a stack trace.

As a general rule, do not use panic!

The panic function yeets control out of the current function and up the call stack until it reaches a function that defers a recover. If no function calls recover, the goroutine (often the entire program) crashes.

func enrichUser(userID string) User {
    user, err := getUser(userID)
    if err != nil {
        panic(err)
    }
    return user
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered from panic:", r)
        }
    }()

    // this panics, but the defer/recover block catches it
    // a truly astonishingly bad way to handle errors
    enrichUser("123")
}

Sometimes new Go developers look at panic/recover, and think, "This is like try/catch! I like this!" Don't be like them.

I use error values for all "normal" error handling, and if I have a truly unrecoverable error, I use log.Fatal to print a message and exit the program.

Frequently Asked Questions

How does Go express errors?

Go programs express errors with error values. A nil error denotes success; a non-nil error denotes failure.

What is the error interface in Go?

An error is any type that implements the Error() string method.

Where should a function return an error?

When something can go wrong, the function should return an error as its last return value.

How do you create a simple error in Go?

The standard library's errors package provides errors.New(), which creates an error from a message.

Should panic handle normal errors in Go?

No. Use error values for normal error handling; as a general rule, do not use panic.