Why does `defer recover()` not catch panics?
Asked Answered
L

3

31

Why does a call to defer func() { recover() }() successfully recover a panicking goroutine, but a call to defer recover() not?

As an minimalistic example, this code doesn't panic

package main

func main() {
    defer func() { recover() }()
    panic("panic")
}

However, replacing the anonymous function with recover directly panics

package main

func main() {
    defer recover()
    panic("panic")
}
Lowson answered 8/4, 2015 at 14:51 Comment(0)
D
10

The Handling panic section mentions that

Two built-in functions, panic and recover, assist in reporting and handling run-time panics

The recover function allows a program to manage behavior of a panicking goroutine.

Suppose a function G defers a function D that calls recover and a panic occurs in a function on the same goroutine in which G is executing.

When the running of deferred functions reaches D, the return value of D's call to recover will be the value passed to the call of panic.
If D returns normally, without starting a new panic, the panicking sequence stops.

That illustrates that recover is meant to be called in a deferred function, not directly.
When it panic, the "deferred function" cannot be the built-in recover() one, but one specified in a defer statement.

DeferStmt = "defer" Expression .

The expression must be a function or method call; it cannot be parenthesized.
Calls of built-in functions are restricted as for expression statements.

With the exception of specific built-in functions, function and method calls and receive operations can appear in statement context.

Desorb answered 8/4, 2015 at 14:56 Comment(0)
M
16

Quoting from the documentation of the built-in function recover():

If recover is called outside the deferred function it will not stop a panicking sequence.

In your second case recover() itself is the deferred function, and obviously recover() does not call itself. So this will not stop the panicking sequence.

If recover() would call recover() in itself, it would stop the panicking sequence (but why would it do that?).

Another Interesting Example:

The following code also doesn't panic (try it on the Go Playground):

package main

func main() {
    var recover = func() { recover() }
    defer recover()
    panic("panic")
}

What happens here is we create a recover variable of function type which has a value of an anonymous function calling the built-in recover() function. And we specify calling the value of the recover variable to be the deferred function, so calling the builtin recover() from that stops the panicing sequence.

Mutable answered 8/4, 2015 at 15:3 Comment(7)
And why they specified recover() that way: maybe it'd be additional implementation work to allow bare defer recover(), and more importantly, silently swallowing all panics is risky in the way ignoring err is: it makes it much easier to have a problem that stops your program from doing what you want and not know what it is.Anabel
@Anabel In one of my applications, I'm writing to a channel, and I don't want to panic if it's closed. Hence defer recover(). The entire routine is basically defer recover(); ch1 <- <- ch2.Trixie
@EthanReesor; noted, but usually the rule is that "Only the sole/final sender (or some delegate thereof) should ever close a channel" (so no sending after the close). Some things achieve this by signalling shutdown some other way (like closing a quit or cancel channel on one end, then checking quit status with a select { case <-quit: /* ...do shutdown stuff... */ default: } on the other). More narrowly, I can't change the spec; if you want to catch 'em all defer func() { recover() } is the only way.Anabel
@EthanReesor Graceful shutdown of parallel stuff really deserves more attention than I can give it now or fits in a comment; what you want to do, and how you do it (closing a channel, close(quit), sync.WaitGroup, etc), sometimes need some thought specific to your situation.Anabel
@Anabel Do you have a reading recommendation for learning good parallel/asynchronous practices for a language like Go? Which is to say, a language that has co/goroutines and channels, not threads.Trixie
Here are some links and vague statements. Not like you have to read all that to make progress, but lots of things to snack on over time. If you have a particular scenario you think might be possible to improve on, SO may be able to help with some info on what you've done and the problem you're solving (not a promise that I have the time, or that any of us will necessarily improve on your approach!).Anabel
is it possible to recover a panic coming from main, deferring the recover function in a test case? Docs samples don't workFrankforter
D
10

The Handling panic section mentions that

Two built-in functions, panic and recover, assist in reporting and handling run-time panics

The recover function allows a program to manage behavior of a panicking goroutine.

Suppose a function G defers a function D that calls recover and a panic occurs in a function on the same goroutine in which G is executing.

When the running of deferred functions reaches D, the return value of D's call to recover will be the value passed to the call of panic.
If D returns normally, without starting a new panic, the panicking sequence stops.

That illustrates that recover is meant to be called in a deferred function, not directly.
When it panic, the "deferred function" cannot be the built-in recover() one, but one specified in a defer statement.

DeferStmt = "defer" Expression .

The expression must be a function or method call; it cannot be parenthesized.
Calls of built-in functions are restricted as for expression statements.

With the exception of specific built-in functions, function and method calls and receive operations can appear in statement context.

Desorb answered 8/4, 2015 at 14:56 Comment(0)
T
2

An observation is that the real problem here is the design of defer and thus the answer should say that.

Motivating this answer, defer currently needs to take exactly one level of nested stack from a lambda, and the runtime uses a particular side effect of this constraint to make a determination on whether recover() returns nil or not.

Here's an example of this:

func b() {
  defer func() { if recover() != nil { fmt.Printf("bad") } }()
}

func a() {
  defer func() {
    b()
    if recover() != nil {
      fmt.Printf("good")
    }
  }()
  panic("error")
}

The recover() in b() should return nil.

In my opinion, a better choice would have been to say that defer takes a function BODY, or block scope (rather than a function call,) as its argument. At that point, panic and the recover() return value could be tied to a particular stack frame, and any inner stack frame would have a nil pancing context. Thus, it would look like this:

func b() {
  defer { if recover() != nil { fmt.Printf("bad") } }
}

func a() {
  defer {
    b()
    if recover() != nil {
      fmt.Printf("good")
    }
  }
  panic("error")
}

At this point, it's obvious that a() is in a panicking state, but b() is not, and any side effects like "being in the first stack frame of a deferred lambda" aren't necessary to correctly implement the runtime.

So, going against the grain here: The reason this doesn't work as might be expected, is a mistake in the design of the defer keyword in the go language, that was worked around using non-obvious implementation detail side effects and then codified as such.

Thermodynamic answered 28/2, 2019 at 18:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.