12

The go runtime can detect panic(nil) and reports an error.

However, I can't detect panic(nil) with recover() in a deferred function because it returns nil, so I cannot differentiate it from normal execution (no panic) as I would test for the return value of recover() to be nil.

For example,

defer func(){
    var err = recover()
    if err != nil {
       // Real serious situation. Panic from inner code.
       // And we may have some critical resources which 
       // must be cleaned-up at any cases.
       // However, this will not be executed for panic(nil) 

       rollback()

       // I am still not sure that how should I treat `panic`…
       // Should I just ignore them?
    }
}()

var err = doTransaction()
if err == nil {
    commit()    // Happy case.
} else {
    rollback()  // Regular execution. Just a lucky case.
}

ROLLBACK is just an example, and I think I can have plenty of critical cases needs cleanup. Well, those cleanup code won't be executed on real program crash too, but I want to defend as much as possible.

How can I detect any panic regardless of its parameter in a deferred function?

eonil
  • 83,476
  • 81
  • 317
  • 516
  • https://github.com/TheApeMachine/errnie/blob/master/v2/guard.go Place guard at top of a method, pass in your rollback function, defer a call to Rescue, panic away. – thenamewasmilo Oct 13 '21 at 03:51

5 Answers5

5

I simply can set a flag before exit.

AFAIK, panic is goroutine-specific, and single goroutine is guaranteed to be in single thread. So synchronization/locking is not required around variable ok. If I'm wrong, please correct me.

func clean(ok *bool) {
    if *ok {
        log.Printf("Execution OK. No panic detected.\n")
    } else {
        var reason = recover()
        log.Printf("Some bad thing happen. reason = %v\n", reason)
        panic("Abnormal exit. Program abandoned. Stack-trace here.")
        debug.PrintStack() // Oops. this will not run.
    }
}

func main() {
    var ok bool = false

    defer clean(&ok)
    panic(nil)

    test1() // Here's the main job.

    ok = true
    log.Printf("All work done. Quit gracefully.\n")
}
eonil
  • 83,476
  • 81
  • 317
  • 516
  • What's the use of having a flag which decides whether a **panic**, the last resort in error handling, is expected or not? What are you actually trying to achieve? This seems wrong on many levels. – nemo Oct 30 '13 at 02:34
  • 1
    @nemo In my case, I needed this to ROLLBACK any SQL command performed in a block if any `panic()` get happen for any reason. I think this is classical *try..catch..clean and rethrow* strategy. Because there's no guarantee about some function *never panic* inside... Any better idea? – eonil Oct 30 '13 at 11:37
  • Please update your question with this information and explain why you are experiencing nil-panics. – nemo Oct 30 '13 at 12:51
  • @nemo I think you are missing the point: when one is running *any* code - especially third-party code - there may be `panic` events. Panic recovery allows one to ID specific error panics - but the problem is `r := recovery()` returns `nil` in the event of *no panic* or `panic(nil)` - and the OP (and I) would like to know if the latter occurred. The current recovery mechanism has no out of the box method to distinguish between the two. – colm.anseo Sep 15 '21 at 19:53
4

Unless I misunderstood your question, deferred function calls will run when panicking, even if the value passed was nil. This is illustrated by the following program:

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("Recover:", recover())
    }()
    panic(nil)
}

You can therefore easily detect if panic(nil) happened by comparing the value returned by recover() to nil.

Edit to answer comment:

Yes, that's true; deferred calls will normally run when a function returns. But they are also run when unwinding the call-stack after a panic().

Edit after question was updated:

You are correct that there is no way to differentiate these cases. On the other hand, panicking with nil doesn't make much sense either - especially because of this limitation.

The only use-case for panic(nil) that I could think of would be to deliberately avoid recovery and force the program to crash with a stack trace. There are more elegant ways to do that though, using the runtime package for instance.

thwd
  • 23,956
  • 8
  • 74
  • 108
  • Ah I'm sorry for unclear question. What I wanted was differentiating `panic(nil)` and normal execution in `defer`red function… – eonil Oct 29 '13 at 15:37
  • Maybe I can just set a flag before exit the function. – eonil Oct 29 '13 at 15:37
4

Check if the proposal 25448 "spec: guarantee non-nil return value from recover" can help.

Calling panic with a nil panic value is allowed in Go 1, but weird.

Almost all code checks for panics with:

     defer func() {
        if e := recover(); e != nil { ... }
     }()

... which is not correct in the case of panic(nil).

The proper way is more like:

     panicked := true
     defer func() {
        if panicked {
              e := recover()
              ...
        }
     }()
     ...
     panicked = false
     return
     ....
     panicked = false
     return

Proposal: make the runtime panic function promote its panic value from nil to something like a runtime.NilPanic global value of private, unassignable type:

package runtime

type nilPanic struct{}

// NilPanic is the value returned by recover when code panics with a nil value.
var NilPanic nilPanic

Proposed in 2018, it just got accepted (Jan. 2023)

The current proposal is that starting in Go 1.21 (say), panic(nil) turns into panic(&runtime.PanicNil{}) during panic (so a variable that happens to be a nil interface does it too, not just the literal text panic(nil)).

panic(nil) is always OK, but recover() returns &runtime.PanicNil{} instead of nil after this change.
If GODEBUG=panicnil=1, then this change is disabled and panic(nil) causes recover() to return nil as it always has.

Assuming #56986 ("proposal: extended backwards compatibility for Go") happens too in Go 1.21, this behavior would only change in modules that say 'go 1.21' in the work module (top-level go.mod).
So when you update from Go 1.20 to Go 1.21, without changing any go.mod lines, you'd still get the old panic(nil) behavior.
When you change your top-level go.mod to say go 1.21, then you get the new behavior, in the whole program.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
2

For most cases, the idiomatic way of if recover() != nil works, but for robustness, or when executing third-party code, it should not be used, because it does not detect panic(nil).

This pattern is how you can check it:

func checkPanic(action func()) (panicked bool, panicMsg interface{}) {
    panicked = true
    defer func() { panicMsg = recover() }
    action()
    panicked = false
    return
}

If action() panics, then the line below is never executed, and panicked stays true. You can check it using the above helper function as follows:

if panicked, msg := checkPanic(func() { /* ... */ }); panicked {
    // handle panic...
} else {
    // happy case :D
}

Or you can directly implement the pattern into the function you want to check, but it might turn out way messier that way.

RmbRT
  • 460
  • 2
  • 10
1

With Go 1.21 it seems panic(nil) no longer panics with nil as pointed out by VonC. As I'm still using Go 1.20, I decided to try RmbRT's answer, but it didn't work. So I decided to instead modify a Try/Catch/Finally from Peter Verhas with RmbRT's answer. It works. Detects nil and non-nil panics, and differentiates that from function returns normally (recover() returns nil).

Here is the code:

type Exception interface{}

func Throw(up Exception) {
    panic(up)
}

func (tcef Tcef) Do() {
    var panicked bool = true

    if tcef.Finally != nil {
        defer tcef.Finally()
    }
    if tcef.Else != nil {
        defer func() {
            if !panicked {
                tcef.Else()
            }
        }()
    }
    if tcef.Catch != nil {
        defer func() {
            if panicked {
                tcef.Catch(recover())
            }
        }()
    }

    tcef.Try()
    panicked = false
}

type Tcef struct {
    Try     func()
    Catch   func(Exception)
    Else    func()
    Finally func()
}

Most of the credits go to Peter (if you see this, thank you so much, I love this solution), I just added a new idea to it. Here is the original version: https://dzone.com/articles/try-and-catch-in-golang. An example of how to use this is also in this page.

EDIT: since I added RmbRT's panicked idea to it, I decided to have it be like Python's case. So I added the Else clause, that only executes if no "Exception" was thrown, and in Python's order (Try/Except(Catch)/Else/Finally).

Edw590
  • 447
  • 1
  • 6
  • 23
  • 1
    Nicely done, upvoted. Seeing `interface{}` (also [known as `any`](https://medium.com/@csaba.ujvari/go-1-18-any-instead-of-interface-ce15dd7bea5d)), I wondered if your code could benefit from generics. In the case of your `Tcf` type and associated functions, it is designed to be very general, catching and handling any kind of panic, whether it is a `nil` value, a string, an error, or any other type of value. So... no: given its general nature, it is not immediately clear how generics would be help. – VonC Aug 16 '23 at 13:01
  • @Edw590 I don't get what problem you encountered with my answer's solution. Could you clarify that? Because I use patterns like that in production and it seems to work for me. – RmbRT Aug 30 '23 at 15:44