Learn Go facts and details by participating in some quizzes. NEW!

The Right Places to Call the recover Function

Panic and recover mechanism has been introduced before, and several panic/recover use cases are shown in the last article. We know that a recover call can only take effect if it is invoked in a deferred function call. However, not all recover calls in deferred function calls can catch alive panics. The remaining of this article will show some useless recover function calls and explain what are the right places to call the recover function.

Let's view an example:
package main

import "fmt"

func main() {
	defer func() {
		defer func() {
			fmt.Println("7:", recover())
		}()
	}()
	defer func() {
		func() {
			fmt.Println("6:", recover())
		}()
	}()
	func() {
		defer func() {
			fmt.Println("1:", recover())
		}()
	}()
	func() {
		defer fmt.Println("2:", recover())
	}()
	func() {
		fmt.Println("3:", recover())
	}()
	fmt.Println("4:", recover())
	defer fmt.Println("5:", recover())
	panic(789)
	defer func() {
		fmt.Println("0:", recover())
	}()
}
Run it, we will find that none of the 8 recover calls in above program recover the program. The program crashes with printing out the following panic stack traces.
1: <nil>
2: <nil>
3: <nil>
4: <nil>
5: <nil>
6: <nil>
7: <nil>
panic: 789

goroutine 1 [running]:
...
Obviously, the 0th recover call (the last one) is not reachable. For others, let's check the rules specified in Go specification firstly.
The return value of recover is nil if any of the following conditions holds:
  • panic's argument was nil;
  • the goroutine is not panicking;
  • recover was not called directly by a deferred function.

Let's ignore the first condition. The second condition covers the 1st/2nd/3rd and 4th recover calls. The third one covers the 5th and 6th recover calls. However, none of the three conditions cover the 7th recover call.

We know the following recover call will catch the panic.
// example2.go
package main

import (
	"fmt"
)

func main() {
	defer func() {
		fmt.Println( recover() ) // 1
	}()

	panic(1)
}

But, what is differences between it and the above recover calls? What is the principle rule to make a recover call take effect?

Firstly, let's learn some concepts and facts.

Concept: Function Call Depth, Goroutine Execution Depth & Panic Depth

Each function call associated to a call depth, which is related the entry function call of the current goroutine. For the main goroutine, the call depth of a function call is related to the program main entry function call. For a goroutine other than the main goroutine, the call depth of a function call is related to the starting function call of the goroutine.

For example:
package main

func main() { // depth 0
	go func() { // depth 0
		func() { // depth 1
		}()

		defer func() { // depth 1
			defer func() { // depth 2
			}()
		}()
	}()

	func () { // depth 1
		func() { // depth 2
			go func() { // depth 0
			}()
		}()

		go func() { // depth 0
		}()
	}()
}

The depth of the call to the function containing the current execution point of a goroutine is called the execution depth of the goroutine.

The depth of a panic is the function call depth the panic has propagated to. Please read the next section for a better understanding.

Fact: Panics Can Only Propagate up to the Function Calls With Shallower Depths

Yes, panics can only propagate up to its callers. Panics never propagate downwards into a deeper function call.
package main

import "fmt"

var o = fmt.Println

func main() { // call depth 0
	defer func() { // call depth 1
		o("Now, the panic is still in call depth 0")
		func() { // call depth 2
			o("Now, the panic is still in call depth 0")
			func() { // call depth 3
				o("Now, the panic is still in call depth 0")
			}()
		}()
	}()

	defer o("Now, the panic is in call depth 0")

	func() { // call depth 1
		defer o("Now, the panic is in call depth 1")
		func() { // call depth 2
			defer o("Now, the panic is in call depth 2")
			func() { // call depth 3
				defer o("Now, the panic is in call depth 3")
				panic(1)
			}()
		}()
	}()
}

So the depth of a panic is monotonically decreasing, it never increases. And, the depth of an alive panic will never be larger than the execution depth of the goroutine.

Fact: New Created Panics Will Suppress Old Panics at the Same Depth

Example:
package main

import "fmt"

func main() {
	defer fmt.Println("program will not crash")

	defer func() {
		fmt.Println( recover() ) // 3
	}()

	defer fmt.Println("now, panic 3 suppresses panic 2")
	defer panic(3)
	defer fmt.Println("now, panic 2 suppresses panic 1")
	defer panic(2)
	panic(1)
}
Outputs:
now, panic 2 suppresses panic 1
now, panic 3 suppresses panic 2
3
program will not crash

In this example, panic 1 is suppressed by panic 2, and then panic 2 is suppressed by panic 3. So, in the end, there is only one active panic, which is panic 3. Panic 3 is recovered, hence the program will not crash.

In one goroutine, there will be at most one active panic at the same time and at the same call depth. In particular, when the execution point runs at the call depth 0 of a goroutine, there will be at most one active panic in the goroutine.

Fact: Multiple Active Panics May Coexist in a Goroutine

Example:
package main

import "fmt"

func main() { // call depth 0
	defer fmt.Println("to crash, for panic 3 is still active")

	defer func() { // call depth 1
		defer func() { // call depth 2
			fmt.Println( recover() ) // 6
		}()

		// The depth of panic 3 is 0,
		// and the depth of panic 6 is 1.
		defer fmt.Println("now, two active panics: 3 and 6")
		defer panic(6) // will suppress panic 5
		defer panic(5) // will suppress panic 4

		// The following panic will not suppress
		// panic 3,  for they have different depths.
		// The depth of panic 3 is 0.
		// The depth of panic 4 is 1.
		panic(4)
	}()

	defer fmt.Println("now, only panic 3 is active")
	defer panic(3) // will suppress panic 2
	defer panic(2) // will suppress panic 1
	panic(1)
}

In this example, panic 6, one of the two active panics, is recovered. But the other active panic, panic 3, is still active at the end of main call, so the program will crash.

The output:
now, only panic 3 is active
now, there are two active panics: 3 and 6
6
program will crash, for panic 3 is still active
panic: 1
	panic: 2
	panic: 3

goroutine 1 [running]
...

Fact: Deeper Panics May Be Recovered at First or Not

(Note: I'm sorry that the code in this section is not correct. It contains a stupid mistake. It is unable to prove the conclusion in the section title. Two panics never coexist in the lifetime of the following program. The correctnesses of the conclusion in the section title and the final rule at the end of this article still need a confirmation from Go core team. This section might be removed after some days.)

Example:
package main

import "fmt"

func demo(recoverShallowerPanicAtFirst bool) {
	fmt.Println("====================")
	defer func() {
		if !recoverShallowerPanicAtFirst{
			// recover panic 1
			defer fmt.Println("panic", recover(), "is recovered")
		}
		defer func() {
			// recover panic 2
			fmt.Println("panic", recover(), "is recovered")
		}()
		if recoverShallowerPanicAtFirst {
			// recover panic 1
			defer fmt.Println("panic", recover(), "is recovered")
		}
		defer fmt.Println("now, two active panics coexist")
		panic(2)
	}()
	panic(1)
}

func main() {
	demo(true)
	demo(false)
}
The output:
====================
now, two active panics coexist
panic 1 is recovered
panic 2 is recovered
====================
now, two active panics coexist
panic 2 is recovered
panic 1 is recovered

Then, What Is the Principle Rule to Make a recover Call Take Effect?

(Note: The correctness of the following rule still needs a confirmation from Go core team.)

The rule is simple:
In a goroutine, assume the caller function of a recover call is f and the depth of the f call is d, then, to make the recover call take effect, the f call must be a deferred call and there must be an active panic at depth d-1. Whether or not the recover call itself is deferred is not important.

That is it. I think It is described better than Go specification. Now you can page up to check why the 7th recover call in the first example doesn't take effect, and some others do in some other examples.


The Go 101 project is hosted on both Github and Gitlab. Welcome to improve Go 101 articles by submitting corrections for all kinds of mistakes, such as typos, grammar errors, wording inaccuracies, description flaws, code bugs and broken links.

If you would like to learn some Go details and facts every serveral days, please follow Go 101's official Twitter account: @go100and1.

The digital versions of this book are available at the following stores and forms: Tapir, the author of Go 101, has spent 3+ years on writing the Go 101 book and maintaining the go101.org website. New contents will continue being added to the book and the website from time to time. If you would like to, you can support the book and the website by making a donation through Paypal ($5, $9, $15, or $25) or buying Tapir a coffee (way 1 and way 2).

You can also support Go 101 by playing Tapir's games.
Cryptocurrency donations are welcome too:
Bitcoin: 1xucQbv5jujFPPwhyg395ri5yV71hx9g9
Ethereum: 0x5dc4aa2c2bbfaadae373dadcfca11b3358912212