Skip to main content
In this tutorial, let’s talk about error handling.
Notice how we say errors and not exceptions - there is no exception handling in Go.
Instead, we can return a built-in error type which is an interface type:
type error interface {
    Error() string
}
Let’s declare a simple Divide function which divides integer a by b:
func Divide(a, b int) int {
	return a/b
}
Now, we want to return an error to prevent division by zero. This brings us to error construction.

Constructing Errors

There are multiple ways to construct errors, but we will look at the two most common ones.

errors package

The first is by using the New function provided by the errors package:
package main

import "errors"

func main() {}

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

	return a/b, nil
}
Notice how we return an error with the result. If there is no error we simply return nil as it is the zero value of an error (since it’s an interface).
Let’s handle the error in our main function:
package main

import (
	"errors"
	"fmt"
)

func main() {
	result, err := Divide(4, 0)

	if err != nil {
		fmt.Println(err)
		// Do something with the error
		return
	}

	fmt.Println(result)
	// Use the result
}

func Divide(a, b int) (int, error) {...}
$ go run main.go
cannot divide by zero
As you can see, we simply check if the error is nil and build our logic accordingly. This is considered quite idiomatic in Go.

fmt.Errorf

Another way to construct errors is by using the fmt.Errorf function. This function is similar to fmt.Sprintf and lets us format our error. But instead of returning a string, it returns an error. It is often used to add context or detail to our errors:
...
func Divide(a, b int) (int, error) {
	if b == 0 {
		return 0, fmt.Errorf("cannot divide %d by zero", a)
	}

	return a/b, nil
}
And it works similarly:
$ go run main.go
cannot divide 4 by zero

Sentinel Errors

Another important technique in Go is defining expected errors so they can be checked explicitly in other parts of the code. These are sometimes referred to as sentinel errors.
package main

import (
	"errors"
	"fmt"
)

var ErrDivideByZero = errors.New("cannot divide by zero")

func main() {...}

func Divide(a, b int) (int, error) {
	if b == 0 {
		return 0, ErrDivideByZero
	}

	return a/b, nil
}
In Go, it is conventional to prefix the variable with Err. For example, ErrNotFound.

Why use sentinel errors?

This becomes useful when we need to execute a different branch of code if a certain kind of error is encountered. We can check explicitly which error occurred using the errors.Is function:
package main

import (
	"errors"
	"fmt"
)

func main() {
	result, err := Divide(4, 0)

	if err != nil {
		switch {
    case errors.Is(err, ErrDivideByZero):
        fmt.Println(err)
				// Do something with the error
    default:
        fmt.Println("no idea!")
    }

		return
	}

	fmt.Println(result)
	// Use the result
}

func Divide(a, b int) (int, error) {...}
$ go run main.go
cannot divide by zero

Custom Errors

This strategy covers most error handling use cases. But sometimes we need additional functionalities such as dynamic values inside our errors. We saw that error is just an interface. Basically, anything can be an error as long as it implements the Error() method which returns an error message as a string. Let’s define a custom DivisionError struct:
package main

import (
	"errors"
	"fmt"
)

type DivisionError struct {
	Code int
	Msg  string
}

func (d DivisionError) Error() string {
	return fmt.Sprintf("code %d: %s", d.Code, d.Msg)
}

func main() {...}

func Divide(a, b int) (int, error) {
	if b == 0 {
		return 0, DivisionError{
			Code: 2000,
			Msg:  "cannot divide by zero",
		}
	}

	return a/b, nil
}
Here, we use errors.As instead of errors.Is to convert the error to the correct type:
func main() {
	result, err := Divide(4, 0)

	if err != nil {
		var divErr DivisionError

		switch {
		case errors.As(err, &divErr):
			fmt.Println(divErr)
			// Do something with the error
		default:
			fmt.Println("no idea!")
		}

		return
	}

	fmt.Println(result)
	// Use the result
}

func Divide(a, b int) (int, error) {...}
$ go run main.go
code 2000: cannot divide by zero

Difference between errors.Is and errors.As

The errors.As function checks whether the error has a specific type, while errors.Is examines if it is a particular error object.
We can also use type assertions but it’s not preferred:
func main() {
	result, err := Divide(4, 0)

	if e, ok := err.(DivisionError); ok {
		fmt.Println(e.Code, e.Msg) // Output: 2000 cannot divide by zero
		return
	}

	fmt.Println(result)
}
Error handling in Go is quite different compared to the traditional try/catch idiom in other languages. But it is very powerful as it encourages developers to actually handle the error in an explicit way, which improves readability.

Build docs developers (and LLMs) love