Panic Handling

The panics package provides low-level utilities for catching and managing panics. While most conc components like WaitGroup and Pool have panic handling built-in, you can use the panics package directly if you are managing your own goroutines and need a structured way to deal with panics.

panics.Catcher

The central type is panics.Catcher. It is used to catch the first panic that occurs across multiple goroutines.

Usage

  1. Create a panics.Catcher.
  2. Wrap the code that might panic in a call to catcher.Try(f func()).
  3. After all calls to Try are complete, check for a panic with Recovered() or propagate it with Repanic().
import "github.com/sourcegraph/conc/panics"

var pc panics.Catcher
i := 0

pc.Try(func() { i += 1 })

// This goroutine will panic
pc.Try(func() { panic("abort!") })

pc.Try(func() { i += 1 })

// Recovered() gets the captured panic.
rc := pc.Recovered()

fmt.Println(i)         // Output: 2
fmt.Println(rc.Value)  // Output: abort!

Propagating Panics with Repanic()

If you simply want to re-throw a captured panic in the calling goroutine (a common pattern in conc), use Repanic().

var pc panics.Catcher
pc.Try(func() { panic("mayday!") })

// This will panic with the recovered value
pc.Repanic()

panics.Recovered

When a panic is caught, it is stored in a panics.Recovered struct. This struct contains not only the original panic value but also stack trace information, which is invaluable for debugging.

// Recovered is a panic that was caught with recover().
type Recovered struct {
    // The original value of the panic.
    Value any

    // The caller list as returned by runtime.Callers when the panic was
    // recovered.
    Callers []uintptr

    // The formatted stacktrace from the goroutine where the panic was recovered.
    Stack []byte
}

Handling Panics as Errors

Sometimes it's useful to treat a panic as a regular error. The AsError() method converts a *Recovered into an error.

If the original panic value was an error, it can be unwrapped using errors.Unwrap.

helper := func() error {
    var pc panics.Catcher
    pc.Try(func() { panic(errors.New("critical error")) })
    return pc.Recovered().AsError()
}

err := helper()
if err != nil {
    // err.Error() includes the full stack trace.
    fmt.Println(err.Error())

    // We can unwrap the original error.
    cause := errors.Unwrap(err)
    fmt.Println(cause) // Output: critical error
}

panics.Try

For the simple case of running a single function and catching a potential panic, you can use the panics.Try helper function. It's a convenient shorthand for creating a Catcher and calling Try once.

import "github.com/sourcegraph/conc/panics"

// No panic
recovered := panics.Try(func() {})
// recovered is nil

// With panic
recovered = panics.Try(func() { panic("oh no") })
// recovered is not nil
fmt.Println(recovered.Value) // Output: oh no