5

I’m trying to catch crashes/panics from go routines that are created in my program, in order to send them to my crash-error-reporting server (such as Sentry/Raygun)

For example,

func main() {

    go func() {
        // Get this panic
        panic("Go routine panic")
    }()
}

The answer states a goroutine cannot recover from a panic in another goroutine.

What would be the idiomatic way to go about it?

Or Nahum
  • 173
  • 1
  • 1
  • 5

1 Answers1

13

You have to "inject" some code into the function that is launched as a new goroutine: you have to call a deferred function in which you call recover(). This is the only way to recover from a panicing state. See related: Why does `defer recover()` not catch panics?

For example:

go func() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Caught:", r)
        }
    }()

    panic("catch me")
}()

This will output (try it on the Go Playground):

Caught: catch me

It is unfeasible to do this in every goroutine you launch, but of course you can move the recovering-logging functionality to a named function, and just call that (but deferred of course):

func main() {
    go func() {
        defer logger()
        panic("catch me")
    }()

    time.Sleep(time.Second)
}

func logger() {
    if r := recover(); r != nil {
        fmt.Println("Caught:", r)
    }
}

This will output the same (try it on the Go Playground).

Yet another, more convenient and even more compact solution is to create a utility function, a "wrapper" which receives the function, and takes care of the recovering.

This is how it could look like:

func wrap(f func()) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Caught:", r)
        }
    }()

    f()
}

And now using it is even simpler:

go wrap(func() {
    panic("catch me")
})

go wrap(func() {
    panic("catch me too")
})

It will output (try it on the Go Playground):

Caught: catch me
Caught: catch me too

Final note:

Note that launching an actual goroutine happens outside of wrap(). This gives the caller the option to decide if a new goroutine is required just by prefixing the wrap() call with go. Usually this approach is preferred in Go. This allows you to execute arbitrary functions by passing them to wrap(), and it will "protect" its execution (by recovering from panics, properly logging / reporting it) even if you do not wish to run it concurrently in a new goroutine. On the other hand if you'd move go inside wrap() it wouldn't even work anymore as the recover() call would not happen on the panicking goroutine.

icza
  • 389,944
  • 63
  • 907
  • 827