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. Below of this atticle 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("6:", recover())
		}()
	}()
	defer func() {
		func() {
			fmt.Println("5:", recover())
		}()
	}()
	func() {
		defer func() {
			fmt.Println("1:", recover())
		}()
	}()
	func() {
		defer fmt.Println("2:", recover())
	}()
	func() {
		fmt.Println("3:", recover())
	}()
	fmt.Println("4:", recover())
	panic(1)
	defer func() {
		fmt.Println("0:", recover()) // never go here
	}()
}
Run it, we will find that none of the 7 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>
panic: 1

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 recover call. However, none of the three conditions cover the 6th 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 principal 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 corresponds to a call depth, which is related the entry function of a goroutine. For the main gorotuine, the call depth of a function call is related to the program main entry function. 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 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 functon call depth the panic has propagated to. Please read the next section for a better understanding.

Fact: Panics Can Only Propagate To The function Calls With Shadow Depths

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

import "fmt"

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

	defer fmt.Println("Now, the panic is in call depth 0")

	func() { // call depth 1
		defer fmt.Println("Now, the panic is in call depth 1")
		func() { // call depth 2
			defer fmt.Println("Now, the panic is in call depth 2")
			func() { // call depth 3
				defer fmt.Println("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: 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 exanple, 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 call depth at any time, especially 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("program will crash, for panic 3 is stll 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, there are two active panics: 3 and 6")
		defer panic(6) // will suppress panic 5
		defer panic(5) // will suppress panic 4
		panic(4) // will not suppress panic 3,
		         // for they have differrent depths.
		         // The depth of panic 3 is 0.
		         // The depth of panic 4 is 1.
	}()

	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 exanple, 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 stll active
panic: 1
	panic: 2
	panic: 3

goroutine 1 [running]
...

Fact: Highest Depth Panics May Be Recovered At First Or Not

Example:
package main

import "fmt"

func demo(recoverHighestPanicAtFirst bool) {
	fmt.Println("====================")
	defer func() {
		if !recoverHighestPanicAtFirst{
			// recover panic 1
			defer fmt.Println("panic", recover(), "is recovered")
		}
		defer func() {
			// recover panic 2
			fmt.Println("panic", recover(), "is recovered")
		}()
		if recoverHighestPanicAtFirst {
			// 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 Principal Rule To Make A recover Call Take Effect?

The rule is simple.
In a goroutine, if 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 better described than Go specification. Now you can page up to check why the 6th 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.

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