diff --git a/panics/panics.go b/panics/panics.go index 1486815..5d4f12d 100644 --- a/panics/panics.go +++ b/panics/panics.go @@ -73,12 +73,29 @@ type RecoveredPanic struct { Stack []byte } -func (c *RecoveredPanic) Error() string { - return fmt.Sprintf("panic: %v\nstacktrace:\n%s\n", c.Value, c.Stack) +// String renders a human-readable formatting of the panic. +func (p *RecoveredPanic) String() string { + return fmt.Sprintf("panic: %v\nstacktrace:\n%s\n", p.Value, p.Stack) } -func (c *RecoveredPanic) Unwrap() error { - if err, ok := c.Value.(error); ok { +// AsError casts the panic into an error implementation. The implementation +// is unwrappable with the cause of the panic, if the panic was provided one. +func (p *RecoveredPanic) AsError() error { + if p == nil { + return nil + } + return &ErrRecoveredPanic{*p} +} + +// ErrRecoveredPanic wraps a RecoveredPanic in an error implementation. +type ErrRecoveredPanic struct{ RecoveredPanic } + +var _ error = (*ErrRecoveredPanic)(nil) + +func (p *ErrRecoveredPanic) Error() string { return p.String() } + +func (p *ErrRecoveredPanic) Unwrap() error { + if err, ok := p.Value.(error); ok { return err } return nil diff --git a/panics/panics_test.go b/panics/panics_test.go index a6a1eba..f75d444 100644 --- a/panics/panics_test.go +++ b/panics/panics_test.go @@ -7,6 +7,7 @@ import ( "sync" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -60,6 +61,26 @@ func ExampleCatcher_callers() { // runtime.goexit } +func ExampleCatcher_error() { + helper := func() error { + var pc Catcher + pc.Try(func() { panic(errors.New("error")) }) + return pc.Recovered().AsError() + } + + if err := helper(); err != nil { + // In normal use cases, you can use err.Error() output directly to + // dump the panic's stack. This is not used in the example because + // its output is machine-specific - instead, we demonstrate getting + // the underlying error that was used for the panic. + if cause := errors.Unwrap(err); cause != nil { + fmt.Printf("helper panicked with an error: %s", cause) + } + } + // Output: + // helper panicked with an error: error +} + func TestCatcher(t *testing.T) { t.Parallel() @@ -70,21 +91,21 @@ func TestCatcher(t *testing.T) { var pc Catcher pc.Try(func() { panic(err1) }) recovered := pc.Recovered() - require.ErrorIs(t, recovered, err1) - require.ErrorAs(t, recovered, &err1) + require.ErrorIs(t, recovered.AsError(), err1) + require.ErrorAs(t, recovered.AsError(), &err1) // The exact contents aren't tested because the stacktrace contains local file paths // and even the structure of the stacktrace is bound to be unstable over time. Just // test a couple of basics. - require.Contains(t, recovered.Error(), "SOS", "error should contain the panic message") - require.Contains(t, recovered.Error(), "panics.(*Catcher).Try", recovered.Error(), "error should contain the stack trace") + require.Contains(t, recovered.String(), "SOS", "formatted panic should contain the panic message") + require.Contains(t, recovered.String(), "panics.(*Catcher).Try", recovered.String(), "formatted panic should contain the stack trace") }) t.Run("not error", func(t *testing.T) { var pc Catcher pc.Try(func() { panic("definitely not an error") }) recovered := pc.Recovered() - require.NotErrorIs(t, recovered, err1) - require.Nil(t, recovered.Unwrap()) + require.NotErrorIs(t, recovered.AsError(), err1) + require.Nil(t, errors.Unwrap(recovered.AsError())) }) t.Run("repanic panics", func(t *testing.T) { @@ -121,3 +142,28 @@ func TestCatcher(t *testing.T) { require.Equal(t, "50", pc.Recovered().Value) }) } + +func TestRecoveredPanicAsError(t *testing.T) { + t.Parallel() + t.Run("as error is nil", func(t *testing.T) { + t.Parallel() + fn := func() error { + var c Catcher + c.Try(func() {}) + return c.Recovered().AsError() + } + err := fn() + assert.Nil(t, err) + }) + + t.Run("as error is not nil nil", func(t *testing.T) { + t.Parallel() + fn := func() error { + var c Catcher + c.Try(func() { panic("oh dear!") }) + return c.Recovered().AsError() + } + err := fn() + assert.NotNil(t, err) + }) +}