Johnny.sh

Errors in Go

In Go, errors are just values. There is an error type, a value of which is just a normal value in your application, just like a float64. Errors can be returned from a function, just like other values.

Notably, Go does not have try/catch syntax. You only have errors as values, which are returned from functions. There are no exceptions in Go, only error values, which you’re expected to handle manually.

v, err := someFunctionCall()
if err != nil {
  // propagate error manually
}
// do stuff with v

The above err != nil is an extremely common pattern in Go.

Just to reiterate: no exceptions, no try/catch, no throw. Errors are just values, handle them accordingly.

Why Is This A Good Thing?

When an error occurs, or something breaks, the control flow of your application is not broken. The function you’re in keeps executing, the application keeps running. Unless you explicitly handle the error in a way which prevents that.

What about panic and recover?

panic is a built-in function which does in fact stop the control flow. When you call panic in a function, the function calls all of its defered function calls, then exits the function prematurely, returning to its caller. The caller, having called a panicked function, then also behaves like a panicked function. The panic is propagated up the stack, until someone does something…

recover is a built-in function which regains control of a panicking function. This is only useful inside of a defered function. The return value of recover() is the error which caused the panic. After calling recover in a defer function, you prevent the caller from then being panicked.

func main() {

    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered. Panic over! Error:\n", r)
        }
    }()

    iWillPanic()

    fmt.Println("After mayPanic()")// this line never runs
}

Constructing errors

So, even though we can’t raise exceptions, we still need to be able to instantiate errors and let the program know something went wrong. How do we do this? There are a couple ways.

1. errors package

import "errors"

func DivideOrDie(nums ...int) (int, error) {
  s := nums[0]
  for _, n := range nums {
    if n == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    s = s/n
  }
  return s, nil
}

2. fmt.Errorf

Using this method is a nice way to format and instantiate an error in one operation.

func Divide(a, b int) (int, error) {
	if b == 0 {
		return 0, fmt.Errorf("cannot divide %d by zero", a)
	}

	return a/b, nil
}

3. Sentinel Errors

Lastly, a popular convention is to use so-called Sentinel errors — reusable error types, exported by the current package, and parseable/identifiable throughout your application.

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
}

We can then check/assert if the error is an instance of a particular error by using the errors.Is function.

It’s typical convention to prefix with Err.

Last modified: September 06, 2022
/about
/uses
/notes
/talks
/projects
/podcasts
/reading-list
/spotify
© 2024