gofiber/fiber · error

%v

Error message

%v

What it means

DefaultPanicHandler is invoked by the recover middleware when a panic occurs and the recovered value r is not an error. It formats r with %v into a new error via fmt.Errorf, losing the original type but producing a stable error message. This becomes the error returned to fiber's error handler. The middleware deliberately avoids the 'panic: ' prefix in its stack-trace handler so gotestsum --rerun-fails is not confused.

Solutions

  1. Find the panic origin from the stack trace logged by the recover middleware's StackTraceHandler (when EnableStackTrace is true).
  2. Replace panic(stringVal) at the source with panic(errors.New(...)) or return an error so the original type is preserved.
  3. Set a custom PanicHandler in recover.Config that type-switches on the recovered value to extract more detail.
  4. Enable EnableStackTrace: true so the goroutine stack is in your logs.
  5. Add unit tests that exercise the panicking code path and assert on the recovered error.

Example fix

// before: library panics with a string, type info lost
panic("invalid config")

// after: panic with an error so DefaultPanicHandler returns it unchanged
panic(errors.New("invalid config"))

// or: install a richer PanicHandler
recover.New(recover.Config{
  PanicHandler: func(_ fiber.Ctx, r any) error {
    if e, ok := r.(error); ok { return e }
    return fmt.Errorf("recovered: %v", r)
  },
})
Defensive patterns

Strategy: try-catch

Type guard

// Type-switch recovered values for richer handling.
func classifyPanic(r any) error {
  switch v := r.(type) {
  case error: return v
  case string: return errors.New(v)
  default: return fmt.Errorf("recovered non-error value: %#v", v)
  }
}

Try / catch

// Install a custom PanicHandler that preserves type info.
app.Use(recover.New(recover.Config{
  EnableStackTrace: true,
  PanicHandler: func(_ fiber.Ctx, r any) error {
    if e, ok := r.(error); ok { return e }
    return fmt.Errorf("recovered: %v", r)
  },
}))

Prevention

When it happens

Trigger: Any panic with a non-error value: panic("boom"), panic(42), panic(map[...]...), panic(nil) (recovered as nil, but recover() returns nil only when no panic — so in practice a non-error typed value). The classic case is panic("some string") from a library that panics on strings.

Common situations: A third-party package panics with a string instead of an error; a goroutine in a handler panics with a custom struct; array/slice out-of-bounds (panics with a runtime.Error, which IS an error and would take the other branch); nil-pointer dereference (runtime.Error, takes the error branch).

Related errors


AI-assisted analysis of gofiber/fiber@a105acad6c (2026-08-11). Data as JSON: /api/errors/105df1e6ca09b1bb. Report an issue: GitHub.

Appendix: source

Thrown at middleware/recover/recover.go:22

	"fmt"
	"os"
	"runtime/debug"

	"github.com/gofiber/fiber/v3"
)

// Must not start with "panic: ": the panic was recovered, and that exact prefix
// makes gotestsum treat the whole run as crashed and skip --rerun-fails.
func defaultStackTraceHandler(_ fiber.Ctx, e any) {
	fmt.Fprintf(os.Stderr, "recovered panic: %v\n\n%s\n", e, debug.Stack())
}

// DefaultPanicHandler returns r directly if it's an error, and creates a new one with the %v verb otherwise.
func DefaultPanicHandler(_ fiber.Ctx, r any) error {
	if err, ok := r.(error); ok {
		return err
	}
	return fmt.Errorf("%v", r)
}

// New creates a new middleware handler
func New(config ...Config) fiber.Handler {
	// Set default config
	cfg := configDefault(config...)

	// Return new handler
	return func(c fiber.Ctx) (err error) { //nolint:nonamedreturns // Uses recover() to overwrite the error
		// Don't execute middleware if Next returns true
		if cfg.Next != nil && cfg.Next(c) {
			return c.Next()
		}

		// Catch panics
		defer func() {
			if r := recover(); r != nil {
				if cfg.EnableStackTrace {

View on GitHub (pinned to a105acad6c)