Learn Go: Panic and Recover in Golang #36

panic and recover can be considered similar to try-catch-finally idiom in other languages, learn how to use in your go programs

Gopher

Lets go!!

Published · Feb 20 2020

Previous Tutorial: Creating Custom Errors in Golang #35



Errors that a program encounters fall into two broad categories: those the programmer has anticipated and those the programmer has not.

The error interface that we have covered in our previous two articles on error handling largely deal with errors that we expect as we are writing Go programs.

The error interface even allows us to acknowledge the rare possibility of an error occurring from function calls, so we can respond appropriately in those situations.
. . .

Introduction

Panics fall into the second category of errors, which are unanticipated by the programmer. These unforeseen errors lead a program to spontaneously terminate and exit the running Go program.

Common mistakes are often responsible for creating panics. In this tutorial, we’ll examine a few ways that common operations can produce panics in Go, and we’ll also see ways to avoid those panics.

We’ll also use defer statements along with the recover function to capture panics before they have a chance to unexpectedly terminate our running Go programs.

panic and recover can be considered similar to try-catch-finally idiom in other languages except that it is rarely used and when used is more elegant and results in clean code.
. . .

When should panic be used?

One important factor is that you should avoid panic and recover and use errors where ever possible. Only in cases where the program just cannot continue execution should a panic and recover mechanism be used.

There are two valid use cases for panic.
  1. An unrecoverable error where the program cannot simply continue its execution. One example would be a web server which fails to bind to the required port. In this case it's reasonable to panic as there is nothing else to do if the port binding itself fails.
  2. A programmer error. Let's say we have a method which accepts a pointer as a parameter and someone calls this method using nil as argument. In this case we can panic as it's a programmer error to call a method with nil argument which was expecting a valid pointer.
. . .

Panic example

When you attempt to access an index beyond the length of a slice or the capacity of an array, the Go runtime will generate a panic.

The following example makes the common mistake of attempting to access the last element of a slice using the length of the slice returned by the len builtin. Try running this code to see why this might produce a panic:
package main import ( "fmt" ) func main() { names := []string{ "lobster", "sea urchin", "sea cucumber", } fmt.Println("My favorite sea creature is:", names[len(names)]) }


This will have the following output:
Output panic: runtime error: index out of range [3] with length 3 goroutine 1 [running]: main.main() /tmp/sandbox879828148/prog.go:13 +0x20

The name of the panic’s output provides a hint: panic: runtime error: index out of range. We created a slice with three sea creatures. We then tried to get the last element of the slice by indexing that slice with the length of the slice using the len builtin function.

Remember that slices and arrays are zero-based; so the first element is zero and the last element in this slice is at index 2. Since we try to access the slice at the third index, 3, there is no element in the slice to return because it is beyond the bounds of the slice.

The runtime has no option but to terminate and exit since we have asked it to do something impossible. Go also can’t prove during compilation that this code will try to do this, so the compiler cannot catch this.

Notice also that the subsequent code did not run. This is because a panic is an event that completely halts the execution of your Go program. The message produced contains multiple pieces of information helpful for diagnosing the cause of the panic.
. . .

Using the panic Builtin Function

We can also generate panics of our own using the panic built-in function. It takes a single string as an argument, which is the message the panic will produce. Typically this message is less verbose than rewriting our code to return an error.

Furthermore, we can use this within our own packages to indicate to developers that they may have made a mistake when using our package’s code. Whenever possible, best practice is to try to return error values to consumers of our package.

Run this code to see a panic generated from a function called from another function:
package main func main() { foo() } func foo() { panic("oh no!") }


The panic output produced looks like:
Output panic: oh no! goroutine 1 [running]: main.foo(...) /tmp/sandbox494710869/prog.go:8 main.main() /tmp/sandbox494710869/prog.go:4 +0x40

Here we define a function foo that calls the panic builtin with the string "oh no!". This function is called by our main function. Notice how the output has the message panic: oh no! and the stack trace shows a single goroutine with two lines in the stack trace: one for the main() function and one for our foo() function.

We’ve seen that panics appear to terminate our program where they are generated. This can create problems when there are open resources that need to be properly closed. Go provides a mechanism to execute some code always, even in the presence of a panic.
. . .

Deferred Functions

Your program may have resources that it must clean up properly, even while a panic is being processed by the runtime. Go allows you to defer the execution of a function call until its calling function has completed execution. Deferred functions run even in the presence of a panic, and are used as a safety mechanism to guard against the chaotic nature of panics.

Functions are deferred by calling them as usual, then prefixing the entire statement with the defer keyword, as in defer sayHello(). Run this example to see how a message can be printed even though a panic was produced:
package main import "fmt" func main() { defer func() { fmt.Println("hello from the deferred function!") }() panic("oh no!") }

The output produced from this example will look like:
Output hello from the deferred function! panic: oh no! goroutine 1 [running]: main.main() /Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55

Within the main function of this example, we first defer a call to an anonymous function that prints the message "hello from the deferred function!". The main function then immediately produces a panic using the panic function. In the output from this program, we first see that the deferred function is executed and prints its message. Following this is the panic we generated in main.

Deferred functions provide protection against the surprising nature of panics. Within deferred functions, Go also provides us the opportunity to stop a panic from terminating our Go program using another built-in function.
. . .

Handling Panics

Panics have a single recovery mechanism—the recover builtin function. This function allows you to intercept a panic on its way up through the call stack and prevent it from unexpectedly terminating your program. It has strict rules for its use, but can be invaluable in a production application.

Since it is part of the builtin package, recover can be called without importing any additional packages:
package main import ( "fmt" "log" ) func main() { divideByZero() fmt.Println("we survived dividing by zero!") } func divideByZero() { defer func() { if err := recover(); err != nil { log.Println("panic occurred:", err) } }() fmt.Println(divide(1, 0)) } func divide(a, b int) int { return a / b }


This example will output:
Output 2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero we survived dividing by zero!

Our main function in this example calls a function we define, divideByZero. Within this function, we defer a call to an anonymous function responsible for dealing with any panics that may arise while executing divideByZero.

Within this deferred anonymous function, we call the recover builtin function and assign the error it returns to a variable. If divideByZero is panicking, this error value will be set, otherwise it will be nil.

By comparing the err variable against nil, we can detect if a panic occurred, and in this case we log the panic using the log.Println function, as though it were any other error.

Following this deferred anonymous function, we call another function that we defined, divide, and attempt to print its results using fmt.Println. The arguments provided will cause divide to perform a division by zero, which will produce a panic.

In the output to this example, we first see the log message from the anonymous function that recovers the panic, followed by the message we survived dividing by zero!. We have indeed done this, thanks to the recover builtin function stopping an otherwise catastrophic panic that would terminate our Go program.

The err value returned from recover() is exactly the value that was provided to the call to panic(). It’s therefore critical to ensure that the err value is only nil when a panic has not occurred.
. . .

Getting stack trace after recover

If we recover a panic, we loose the stack trace about the panic. Even in the program above after recovery, we lost the stack trace.

There is a way to print the stack trace using the PrintStack function of the Debug package
package main

import (
"fmt"
"log"
"runtime/debug"
)

func main() {
divideByZero()
fmt.Println("we survived dividing by zero!")
}

func divideByZero() {
defer func() {
if err := recover(); err != nil {
log.Println("panic occurred:", err)
debug.PrintStack()
}
}()
fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
return a / b
}


In the program above, we use debug.PrintStack() in line no.11 to print the stack trace.

This program will output,
2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero goroutine 1 [running]: runtime/debug.Stack(0xec0c0, 0x2579, 0x43c0c0, 0x2) /usr/local/go/src/runtime/debug/stack.go:24 +0xc0 runtime/debug.PrintStack() /usr/local/go/src/runtime/debug/stack.go:16 +0x20 main.divideByZero.func1() /tmp/sandbox255375059/prog.go:18 +0xc0 panic(0x1053e0, 0x1dd4d0) /usr/local/go/src/runtime/panic.go:679 +0x240 main.divide(...) /tmp/sandbox255375059/prog.go:25 main.divideByZero() /tmp/sandbox255375059/prog.go:21 +0x60 main.main() /tmp/sandbox255375059/prog.go:10 +0x20 we survived dividing by zero!

From the output you can understand that first the panic is recovered and Recovered runtime error: index out of range is printed. Following that the stack trace is printed. Then normally returned from main is printed after the panic has recovered.
. . .

Panic, Recover and Goroutines

Recover works only when it is called from the same goroutine. It's not possible to recover from a panic that has happened in a different goroutine. Let's understand this using an example.
package main import ( "fmt" "time" ) func recovery() { if r := recover(); r != nil { fmt.Println("recovered:", r) } } func a() { defer recovery() fmt.Println("Inside A") go b() time.Sleep(1 * time.Second) } func b() { fmt.Println("Inside B") panic("oh! B panicked") } func main() { a() fmt.Println("normally returned from main") }


In the program above, the function b() panics in line no. 23. The function a() calls a deferred function recovery() which is used to recover from panic. The function b() is called as a separate goroutine from line no. 17. and the Sleep in the next line is just to ensure that the program does not terminate before b() has finished running.

What do you think will be the output of the program. Will the panic be recovered? The answer is no. The panic will not be recovered. This is because the recovery function is present in a different gouroutine and the panic is happening in function b() in a different goroutine. Hence recovery is not possible.

Running this program will output,
Inside A Inside B panic: oh! B panicked goroutine 5 [running]: main.b() /tmp/sandbox388039916/main.go:23 +0x80 created by main.a /tmp/sandbox388039916/main.go:17 +0xc0

You can see from the output that the recovery has not happened.

If the function b() was called in the same goroutine then the panic would have been recovered.

If the line no. 17 of the program is changed from
go b()

to
b()

the recovery will happen now since the panic is happening in the same goroutine. If the program is run with the above change it will output,
Inside A Inside B recovered: oh! B panicked normally returned from main
. . .

Conclusion

We have seen a number of ways that panics can be created in Go and how they can be recovered from using the recover builtin. While you may not necessarily use panic yourself, proper recovery from panics is an important step of making Go applications production-ready.
. . .
Next Tutorial: Core Packages in Golang #37

. . .

Chris Gregori

Mar 16 2019

Write your response...

On a mission to build Next-Gen Community Platform for Developers