Cookies management by TermsFeed Cookie Consent

❤️‍🩹 Recover function in Go

introduction syntax builtin

The recover() is a built-in function in Go that stops the program abort sequence invoked by the call of the panic() function. It restores the normal execution of the application and allows for handling the error passed to the panic(). In other words, recover() “catches” panic-type errors and allows you to handle them instead of terminating the application.

Panic and Defer

The recover() works closely with the panic() function and the defer statement.

The panic() function is used in Go to report an unrecoverable state in the application that prevents it from running any longer. In general, there are two types of sources of such a state in an application:

A toy example showing how the panic() function works for the case when the application lacks access to resources:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "errors"
    "fmt"
)

func connectToDB() error {
    return errors.New("error while connecting to db")
}

func main() {
    err := connectToDB()
    if err != nil {
        panic(err)
    }

    fmt.Println("connected to db")
}

Output:

panic: error while connecting to db

goroutine 1 [running]:
main.main()
        main.go:15 +0x49
exit status 2

In the example, the connectToDB() function always returns an error that we use as an argument to the panic(). As a result, the last line of the main(), printing the "connected to db" string is never executed because the program starts the panicking sequence earlier, which results in logging the error to standard output and terminating the application.

To define what the panicking sequence is and how it works, we must first learn more about the defer statement.


The defer statement ensures that a function is executed after the surrounding function returns (the one in which defer was called). It does not matter whether the surrounding function ended without error or was interrupted by a panic. The defer guarantees that the function following this keyword will be executed.

Look at the example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

func foo() {
    defer fmt.Println("Bye foo")
    fmt.Println("Hello foo")
    bar()
    fmt.Println("After bar")
}

func bar() {
    defer fmt.Println("Bye bar")
    fmt.Println("Hello bar")
    baz()
    fmt.Println("After baz")
}

func baz() {
    defer fmt.Println("Bye baz")
    fmt.Println("Hello baz")
    panic("panic in baz")
}

func main() {
    foo()
    fmt.Println("After foo")
}

As you can see, we call a sequence of three functions. The main() function calls foo(), which then calls bar(), and the bar() calls baz(). In each of them, we print out the string "Hello <function-name>" and in the first line declare the deferred function, which should print the string "Bye <function-name>". After each of the three functions call, we print the string "After <function-name>". Moreover, the baz() invokes a panic with the string "panic in baz".

The program prints the following result to the output:

Hello foo
Hello bar
Hello baz
Bye baz
Bye bar
Bye foo
panic: panic in baz

goroutine 1 [running]:
main.baz()
        main.go:20 +0xb8
main.bar()
        main.go:14 +0xaa
main.foo()
        main.go:8 +0xaa
main.main()
        main.go:24 +0x17
exit status 2

In the beginning, everything works as expected - the foo(), bar(), and baz() functions are executed one by one and print out the "Hello <function-name>" messages. Inside the baz() function, there is a panic, but it is not immediately visible in the output. Instead, the baz(), bar() and foo() calls the deferred functions in sequence omitting the "After <function-name>" string printing.

That is how the panic() works - it starts the panicking sequence by immediately stopping the execution of the current function and starting to unwind the current goroutine function stack, running any deferred functions along the way. If this unwinding reaches the top of the stack, the panic is logged, and the program dies.

The diagram below shows how the panic() function and defer statement work in our example:

Diagram on how the panic and defer work together

So, as a result of the panicking sequence, we can see the calls to defer statement functions that print the "Bye <function-name>" messages. During the panicking, the program do not call any other function than in the defer statement - it did not print out the "After <function-name>" strings. The panic itself is logged at the end when it reaches the top of the function stack. Because of that, you can see this log and stack trace as the last message in the output.

Of course, as we mentioned at the beginning, the defer statement works regardless of whether panic occurs in the program or not. To check this, remove panic() from the app and trace the output. Naturally, this also works the other way around, meaning you also do not have to use defer when calling panic().

Recover from a Panic

Sometimes you may want the panicking to abort, the application to return to normal execution, and the whole sequence of unwinding the goroutine function stack and terminating the application to stop. This is what the built-in recover() function is for.

The recover() restores the normal execution of the program by stopping the panicking sequence. It returns the error value passed to the call of the panic(), or nil if the goroutine is not panicking.

The panic() can take any value as an argument, so the recover() also returns the value of type any. Since Go 1.18, any is an alias for interface{}.

The recover() is useful in any case, where a single panic should not terminate the entire program. For example, a critical error in one of the web server client connections should not crash the server app.

It is also used for handling errors in the recursive function stack. If an error occurs at some level of a call to such a function, it usually needs to be handled at the top. To do this, report the panic to unwind the stack to the top-level function call and then recover() from the panic to handle the error.

When you store your application logs, the recover() can also be used to catch a panic and save the panic message to the storage.

Since you already know what the recover() is and how it works, it’s time to show an example how to use it.

The following program is the same as the previous one except that this time we recover from the panic in the baz() function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
    "fmt"
)

func foo() {
    defer fmt.Println("Bye foo")
    fmt.Println("Hello foo")
    bar()
    fmt.Println("After bar")
}

func bar() {
    defer fmt.Println("Bye bar")
    fmt.Println("Hello bar")
    baz()
    fmt.Println("After baz")
}

func baz() {
    defer recoverPanic()
    defer fmt.Println("Bye baz")
    fmt.Println("Hello baz")
    panic("panic in baz")
}

func recoverPanic() {
    if err := recover(); err != nil {
        fmt.Printf("RECOVERED: %v\n", err)
    }
}

func main() {
    foo()
    fmt.Println("After foo")
}

Output:

Hello foo
Hello bar
Hello baz
Bye baz
RECOVERED: panic in baz
After baz
Bye bar
After bar
Bye foo
After foo

In the first line of the baz() there is a deferred panic recovery function recoverPanic() which checks whether the recover() returns a non-nil result. If so, then the panic argument is printed to the standard output with the prefix RECOVERED:.

In the output you can see that the application has indeed recovered the panic, as it returns to normal execution and prints the strings "After baz", "After bar", and "After foo".

Our sample application now looks like this in the diagram:

Diagram on how the recover works

You may wonder why we called the recover() in a deferred function in baz(). Well, this is the first rule when using the recover() function:

The recover() only works in a deferred function.

This is because during the returning to the caller in panicking sequence, the only code that runs is deferred functions, so the recover() will not run elsewhere.

The second rule is:

The recover() only works in the same goroutine where panic() was thrown.

Look at the example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "fmt"
    "time"
)

func barRecover() {
    fmt.Printf("RECOVER: %v\n", recover())
}

func bar() {
    defer fmt.Println("deferred from bar")
    panic("panic in bar")
}

func main() {
    defer barRecover()
    go func() {
        bar()
    }()
    time.Sleep(5 * time.Second)
}

Output:

deferred from bar
panic: panic in bar

goroutine 6 [running]:
main.bar()
       recover/main.go:14 +0x73
main.main.func1()
        recover/main.go:20 +0x17
created by main.main
        recover/main.go:19 +0x45
exit status 2

In the main(), we declared a call to the deferred function barRecover(), which recovers from a panic. Then, we called the bar() function that panics in a new separate goroutine. As you can see in the output, the panic is not recovered since it was invoked by a new goroutine, and the recover() was in the main goroutine. In the diagram, it looks like this:

Diagram on how the recover only works in the same goroutine where panic was thrown

When you replace the bar() call in a new goroutine:

19
20
21
go func() {
    bar()
}()

with a regular call:

19
bar()

then the panic will be recovered and the output you will see will look like this:

deferred from bar
RECOVER: panic in bar

As a rule of thumb, remember this scheme and that you can only call recover() in the same goroutine as panic(). This will save you a lot of debugging time.

Conclusion

The recover() function is a great mechanism in Go to avoid killing the application when unrecoverable errors occur and handle them so that the application can continue to run. In summary, you should remember the 3 most important things about this function:


Thank you for being on our site 😊. If you like our tutorials and examples, please consider supporting us with a cup of coffee and we'll turn it into more great Go examples.

Have a great day!

Buy Me A Coffee

🦥 Ternary operator in Go

Learn how to substitute a ternary operator not available in Go
introduction syntax

➰ Foreach loop in Go

Learn how to construct the popular programming loop
introduction loop syntax

🫘 Count the occurrences of an element in a slice in Go

Learn how to count elements in a slice that meet certain conditions
introduction generics generics-intro