1 package panics
2 3 import (
4 "fmt"
5 "runtime"
6 "runtime/debug"
7 "sync/atomic"
8 )
9 10 // Catcher is used to catch panics. You can execute a function with Try,
11 // which will catch any spawned panic. Try can be called any number of times,
12 // from any number of goroutines. Once all calls to Try have completed, you can
13 // get the value of the first panic (if any) with Recovered(), or you can just
14 // propagate the panic (re-panic) with Repanic().
15 type Catcher struct {
16 recovered atomic.Pointer[Recovered]
17 }
18 19 // Try executes f, catching any panic it might spawn. It is safe
20 // to call from multiple goroutines simultaneously.
21 func (p *Catcher) Try(f func()) {
22 defer p.tryRecover()
23 f()
24 }
25 26 func (p *Catcher) tryRecover() {
27 if val := recover(); val != nil {
28 rp := NewRecovered(1, val)
29 p.recovered.CompareAndSwap(nil, &rp)
30 }
31 }
32 33 // Repanic panics if any calls to Try caught a panic. It will panic with the
34 // value of the first panic caught, wrapped in a panics.Recovered with caller
35 // information.
36 func (p *Catcher) Repanic() {
37 if val := p.Recovered(); val != nil {
38 panic(val)
39 }
40 }
41 42 // Recovered returns the value of the first panic caught by Try, or nil if
43 // no calls to Try panicked.
44 func (p *Catcher) Recovered() *Recovered {
45 return p.recovered.Load()
46 }
47 48 // NewRecovered creates a panics.Recovered from a panic value and a collected
49 // stacktrace. The skip parameter allows the caller to skip stack frames when
50 // collecting the stacktrace. Calling with a skip of 0 means include the call to
51 // NewRecovered in the stacktrace.
52 func NewRecovered(skip int, value any) Recovered {
53 // 64 frames should be plenty
54 var callers [64]uintptr
55 n := runtime.Callers(skip+1, callers[:])
56 return Recovered{
57 Value: value,
58 Callers: callers[:n],
59 Stack: debug.Stack(),
60 }
61 }
62 63 // Recovered is a panic that was caught with recover().
64 type Recovered struct {
65 // The original value of the panic.
66 Value any
67 // The caller list as returned by runtime.Callers when the panic was
68 // recovered. Can be used to produce a more detailed stack information with
69 // runtime.CallersFrames.
70 Callers []uintptr
71 // The formatted stacktrace from the goroutine where the panic was recovered.
72 // Easier to use than Callers.
73 Stack []byte
74 }
75 76 // String renders a human-readable formatting of the panic.
77 func (p *Recovered) String() string {
78 return fmt.Sprintf("panic: %v\nstacktrace:\n%s\n", p.Value, p.Stack)
79 }
80 81 // AsError casts the panic into an error implementation. The implementation
82 // is unwrappable with the cause of the panic, if the panic was provided one.
83 func (p *Recovered) AsError() error {
84 if p == nil {
85 return nil
86 }
87 return &ErrRecovered{*p}
88 }
89 90 // ErrRecovered wraps a panics.Recovered in an error implementation.
91 type ErrRecovered struct{ Recovered }
92 93 var _ error = (*ErrRecovered)(nil)
94 95 func (p *ErrRecovered) Error() string { return p.String() }
96 97 func (p *ErrRecovered) Unwrap() error {
98 if err, ok := p.Value.(error); ok {
99 return err
100 }
101 return nil
102 }
103