Recover in Golang

Daniel Milde
Outreach Prague
Published in
5 min readNov 24, 2023
Image generated by Mid Journey

Recover is one of the less used but very interesting and powerful features of Golang. Let’s see how it works and how we in Outreach.io use it to handle errors in kubernetes.

Panic/defer/recover are basically Golang alternatives to throw/finally/catch concepts from other programming languages. They share a common ground but differ in some important details.

Defer

To fully understand recover, we need to talk about defer statements first. The defer keyword is prepended to a function call and it makes the call to be executed just before the current function returns. When we use multiple defer statements in a function, they are executed in last-in-first-out order, which makes creating cleanup logic very easy as shown in the example below.

package main

import (
"context"
"database/sql"
"fmt"
)

func readRecords(ctx context.Context) error {
db, err := sql.Open("sqlite3", "file:test.db?cache=shared&mode=memory")
if err != nil {
return err
}
defer db.Close() // this function call will be executed third when the readRecords function returns

conn, err := db.Conn(ctx)
if err != nil {
return err
}
defer conn.Close() // this function call will be executed second

rows, err := conn.QueryContext(ctx, "SELECT id FROM users")
if err != nil {
return err
}
defer rows.Close() // this function call will be executed first

for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return err
}
fmt.Println("ID:", id)
}
return nil
}

func main() {
readRecords(context.Background())
}

Panic

The second subject we need to talk about is panic, which is a function that causes the current goroutine switch into panicking mode. The normal flow of execution in the current function is stopped and only defer statements are executed, then doing the same for the caller function, thus bubbling up the stack to the top (main) function and then crashing the program. Panic can be called directly (passing one value as argument) or can be caused by runtime errors. E.g. by nil pointer dereference:

package main

import "fmt"

func main() {
var x *string
fmt.Println(*x)
}
// panic: runtime error: invalid memory address or nil pointer dereference

Recover

Recover is a built-in function that gives us the possibility to regain control when panicking. It does have effect only when called inside a deferred function. It always returns just nil when called outside of one. If we are in panicking mode, the call to recover returns the value passed to the panic function. Basic example:

package main

import "fmt"

func main() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\n", r)
}
}()

panic("spam, egg, sausage, and spam")
}
// Recovered: spam, egg, sausage, and spam

We can recover from runtime errors in the same way:

package main

import "fmt"

func main() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\n", r)
}
}()

var x *string
fmt.Println(*x)
}
// Recovered: runtime error: invalid memory address or nil pointer dereference

The type of value returned by recover is error in this case (runtime.errorString to be more precise).

There is one limitation: We cannot return values directly from the recover block, because the return statement inside the recover block returns only from the deferred function, not from the surrounding function itself:

package main

import "fmt"

func foo() int {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\n", r)
return 1 // "too many return values" because we return from the anonymous function only
}
}()

panic("spam, egg, sausage, and spam")
}

func main() {
x := foo()
fmt.Println(x)
}

If we want to change the value returned by the function, we need to use named return values:

package main

import "fmt"

func foo() (ret int) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\n", r)
ret = 1
}
}()

panic("spam, egg, sausage, and spam")
}

func main() {
x := foo()
fmt.Println("value:", x)
}
// Recovered: spam, egg, sausage, and spam
// value: 1

A more real-world example doing the conversion from panic to normal error might like like this:

package main

import (
"fmt"

"github.com/google/uuid"
)

// processInput tries to convert input string into uuid.UUID
// it converts panic into error
func processInput(input string) (u uuid.UUID, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()

// some logic (can be 3rd party as well) that can panic, e.g.:
u = uuid.MustParse(input)
return u, nil
}

func main() {
u, err := processInput("xxx")
if err != nil {
fmt.Println(err)
}
fmt.Println(u)
}
// panic: uuid: Parse(xxx): invalid UUID length: 3
// 00000000-0000-0000-0000-000000000000

Now let’s try something a little more complicated. Suppose we are running in kubernetes, and we want to write a general recover function that handles all uncaught panics and runtime errors, and collects stack traces for them so that we can log them in a structured way (as json).

package main

import (
"fmt"
"log/slog"
"os"

"github.com/pkg/errors"
)

func foo() string {
var s *string
return *s
}

func handlePanic(r interface{}) error {
var errWithStack error
if err, ok := r.(error); ok {
errWithStack = errors.WithStack(err)
} else {
errWithStack = errors.Errorf("%+v", r)
}
return errWithStack
}

func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

defer func() {
if r := recover(); r != nil {
err := handlePanic(r)
logger.Error(
"panic occurred",
"msg", err.Error(),
"stack", fmt.Sprintf("%+v", err),
)
}
}()

fmt.Println(foo())
}

// {
// "time":"2009-11-10T23:00:00Z",
// "level":"ERROR",
// "msg":"panic occurred",
// "msg":"runtime error: invalid memory address or nil pointer dereference",
// "stack":"runtime error: invalid memory address or nil pointer dereference\nmain.handlePanic\n\t/tmp/sandbox239055659/prog.go:19\nmain.main.func1..."
// }

That’s it for today! The recover function is not the daily bread of the Golang developer, but as you can see it can be very useful in some situations.

Update

It’s important to note, that while panic/recover can be compared to throw/except in other languages, they should be used in different situations. Panic/recover should never be used for normal flows, e.g. for user-defined errors, such as validation errors, etc. So if you expect an error to occur, use the standard error return value for it.

Don’t forget to check our previous blog posts about Go:

--

--