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.
panic
and recover
?
What about 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 defer
ed 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 defer
ed 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.
errors
package
1. 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
}
fmt.Errorf
2. 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
.