panics.go raw

   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