Errors in Go
In Go, errors are values and act like any other data type, so they can be function parameters, return values, and so on. At its core, an error is an object that has an Error() string
method, thus satisfying the Error interface. Additionally, they can be wrapped with extra information during their usage by formatting them with %w
, allowing for error chains that can be used to trace the origin of an error.
When a function call returns an error, it’s the caller’s responsibility to check it and take appropriate action. Possible actions are to propagate the error, retry the operation, log and continue, or log and stop the program.
When using errors constructed with errors.New
, they can be checked for their specific type with errors.Is
, which will report whether any error in the chain matches:
var ErrFoo = errors.New("foo")
func main() {
err := bar()
if err != nil {
if errors.Is(err, ErrFoo) {
fmt.Printf("foo error: %v\n", err)
} else {
fmt.Printf("random error: %v\n", err)
}
}
}
func bar() error {
err := foo()
if err != nil {
return fmt.Errorf("error in bar: %w", err)
}
return nil
}
func foo() error {
return ErrFoo
}
// Output:
// foo error: error in bar: foo
Errors should not be checked with ==
as it does not perform unwrapping, and only the first error of the chain will be compared:
var ErrFoo = errors.New("foo")
func main() {
err := bar()
fmt.Println("is error foo with errors.Is?", errors.Is(err, ErrFoo))
fmt.Println("is error foo with ==?", err == ErrFoo)
}
func bar() error {
err := foo()
if err != nil {
return fmt.Errorf("error in bar: %w", err)
}
return nil
}
func foo() error {
return ErrFoo
}
// Output:
// is error foo with errors.Is? true
// is error foo with ==? false
Errors can also be constructed using custom objects that satisfy the error interface, and can be checked with errors.As
, which will check for the first error in the chain that matches, and set it to the custom error value:
type ErrorA struct {
Err error
Message string
}
func (c *ErrorA) Error() string {
return c.Message
}
type ErrorB struct {
Err error
Message string
}
func (c *ErrorB) Error() string {
return c.Message
}
var ErrFoo = errors.New("foo")
func main() {
errA := &ErrorA{}
errB := &ErrorB{}
err := bar()
fmt.Println("is error A?", errors.As(err, &errA))
fmt.Println(errA.Err.Error())
fmt.Println("is error B?", errors.As(err, &errB))
fmt.Println(errB.Err.Error()) // This will cause a panic because `err` is not of type `ErrorB` so `errB` is nil
}
func bar() error {
err := foo()
if err != nil {
return &ErrorA{
Err: fmt.Errorf("error in bar: %w", err),
Message: err.Error(),
}
}
return nil
}
func foo() error {
return ErrFoo
}
// Output:
// is error A? true
// error in bar: foo
// is error B? false
// panic: runtime error: invalid memory address or nil pointer dereference
Panics
For errors that impede the continuation of a program, there is a built-in function panic
that in effect creates a run-time error that will stop the program.
func main() {
panic("boom")
}
// Output:
// panic: boom
// goroutine 1 [running]:
// main.main()
// /Users/lewis/repositories/lewislbr/go-sandbox/test/main.go:4 +0x38
// exit status 2
When a panic is triggered, it immediately stops execution of the current function and begins unwinding the stack of the goroutine, running any deferred functions along the way. When that unwinding reaches the top of the goroutine’s stack, the program exits.
func main() {
defer fmt.Println("bye")
panic("boom")
}
// Output:
// bye
// panic: boom
// goroutine 1 [running]:
// main.main()
// /Users/lewis/repositories/lewislbr/go-sandbox/test/main.go:4 +0x38
// exit status 2
However, it is possible to use the built-in function recover
to regain control of the goroutine and resume normal execution.
func main() {
defer func() {
err := recover()
if err != nil {
fmt.Println("got panic:", err)
}
}()
panic("boom")
}
// Output:
// got panic: boom
One application of recover is to shut down a failing goroutine inside a program without killing the other executing goroutines.
func worker(tasks <-chan *Task) {
for task := range tasks {
go safelyDo(task)
}
}
func safelyDo(task *Task) {
defer func() {
err := recover()
if err != nil {
log.Println("task failed:", err)
}
}()
do(task)
}