Skip to content

Chapter 21: Panic Recovery

Description

Test panic recovery with recover() inside defer to convert panics into errors. Production code uses defer/recover to prevent crashes from division by zero, nil pointer dereference, or invalid input. Tests exercise both the panic path (via assert.Panics) and the recovered-error path (via SafeDivide, SafeExecute).

Code

var ErrPanic = errors.New("panic recovered")

func SafeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("%w: %v", ErrPanic, r)
        }
    }()
    return a / b, nil
}

func SafeExecute(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("%w: %v", ErrPanic, r)
        }
    }()
    fn()
    return nil
}

Test

func TestSafeDivide(t *testing.T) {
    t.Run("normal division", func(t *testing.T) {
        r, err := SafeDivide(10, 3)
        require.NoError(t, err)
        assert.Equal(t, 3, r)
    })
    t.Run("division by zero", func(t *testing.T) {
        _, err := SafeDivide(1, 0)
        assert.Error(t, err)
        assert.ErrorIs(t, err, ErrPanic)
        assert.Contains(t, err.Error(), "integer divide by zero")
    })
}

func TestMustParse(t *testing.T) {
    t.Run("valid number", func(t *testing.T) {
        n, err := MustParse("42")
        require.NoError(t, err)
        assert.Equal(t, 42, n)
    })
    t.Run("empty string", func(t *testing.T) {
        _, err := MustParse("")
        assert.Error(t, err)
        assert.ErrorIs(t, err, ErrPanic)
    })
    t.Run("invalid character", func(t *testing.T) {
        _, err := MustParse("12a34")
        assert.Error(t, err)
        assert.ErrorIs(t, err, ErrPanic)
    })
}

func TestPanicIfNegative(t *testing.T) {
    assert.Equal(t, 10, PanicIfNegative(5))
    assert.Panics(t, func() { PanicIfNegative(-1) })
    assert.PanicsWithValue(t, "negative value not allowed", func() {
        PanicIfNegative(-5)
    })
}

func TestSafeExecute(t *testing.T) {
    t.Run("no panic", func(t *testing.T) {
        called := false
        err := SafeExecute(func() { called = true })
        require.NoError(t, err)
        assert.True(t, called)
    })
    t.Run("panic recovered", func(t *testing.T) {
        err := SafeExecute(func() { panic("boom") })
        assert.Error(t, err)
        assert.ErrorIs(t, err, ErrPanic)
    })
}

Testing Approach

Panic recovery testing:

  1. Named returns for defer(result int, err error) gives the deferred closure access to the return values. The deferred recover sets err before the function returns.
  2. assert.Panics for expected panicsPanicIfNegative(-1) should panic. assert.Panics catches it and verifies the panic happened. PanicsWithValue checks the exact panic value.
  3. assert.ErrorIs for sentinel errorsErrPanic wraps the original panic value. Tests assert ErrorIs(ErrPanic) to confirm recovery happened, plus Contains for the underlying reason.
  4. Safe wrapper patternSafeExecute(fn) demonstrates recovering a panic from any function. Useful for wrapping third-party code or callbacks that might panic.

View source code on GitHub