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