Hardening a Go Fiber v2 Service Against Handler Panics
A single panic in a Fiber handler can crash the process and drop in-flight requests. This guide shows how to register recovery middleware early, normalize errors with a custom handler, and verify the service stays alive.
09 Jul 2025, 00:40 UTC

The Problem: One Panic Takes Down the Process
An unhandled panic inside a request handler does not just fail one request—it crashes the entire Go process. Every in-flight connection is dropped, and the server loses the opportunity to log what went wrong. In a Fiber v2 service, the defense is to intercept panics before they reach the runtime, return a generic HTTP 500 to the client, and preserve structured context server-side.
Desired Outcome
By the end of this guide, the service will meet three conditions: unexpected panics in handlers leave the process running, clients receive a safe generic error response without stack traces, and the logs contain enough context—HTTP method, path, timestamp, and request ID—to reconstruct the failure during post-mortem analysis.
Necessary Prerequisites
- Go toolchain installed and a project initialized with
go mod init. - Fiber v2 pinned in
go.mod(for example,github.com/gofiber/fiber/v2). The middleware and error handler signatures differ from Fiber v1, so confirm the dependency version before copying code. - An existing HTTP service with at least one route that can panic.
Focused Procedure
Register Recovery Middleware First
Middleware order is critical. The recovery middleware must wrap every route so that panics in handlers are intercepted by the framework rather than crashing the process. In Fiber v2, register recover.New() before any route definitions.
app := fiber.New()
// Must come before routes
app.Use(recover.New())
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("ok")
})
Run this from your project root with go run .. The process listens as an unprivileged user on the configured port. The risk of skipping this step is that a single handler panic terminates the server.
Normalize Errors With a Custom Handler
Fiber routes return error values. A custom error handler centralizes how those errors become HTTP responses. Configure it on the app instance so every route benefits from the same normalization.
app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {
// Log detailed context server-side
requestID := c.Locals("requestid")
log.Printf("method=%s path=%s request_id=%v error=%v",
c.Method(), c.Path(), requestID, err)
// Return a generic message to the client
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "internal server error",
})
},
})
The handler itself must be panic-free. If the error handler panics, the process will crash. Keep the logic minimal and defensive.
Correlate Requests With Request IDs
Request IDs make post-mortem analysis possible. Add the request ID middleware early in the chain, then reference the ID in your error handler and logs.
app.Use(requestid.New())
With this in place, the requestid value is available in c.Locals("requestid") inside handlers and the error handler.
Add a Deliberate Panic Test Route
Verification requires a reproducible failure. Add a route that panics on purpose so you can confirm the recovery middleware works.
app.Get("/panic-test", func(c *fiber.Ctx) error {
panic("deliberate test panic")
})
This route changes application state by exposing a failure endpoint. Remove it before deploying to production.
Wire Graceful Shutdown
Recovery keeps the process alive during requests, but deployment and restarts still send SIGTERM. Handle the signal so in-flight requests finish and logs are flushed before exit.
go func() {
if err := app.Listen(":3000"); err != nil {
log.Fatalf("listen error: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down server...")
if err := app.ShutdownWithTimeout(10 * time.Second); err != nil {
log.Fatalf("shutdown error: %v", err)
}
log.Println("server exited")
Run the binary as a standard user. The ShutdownWithTimeout call waits up to the specified duration for active requests to complete. If the timeout is exceeded, the server exits and remaining connections are dropped.
Expected Checks
| Check | Action | Expected Result |
|---|---|---|
| Panic recovery | curl -i http://localhost:3000/panic-test | HTTP 500 with a generic JSON body; no stack trace in the response. |
| Process survival | Repeat the curl command | The server continues accepting new requests; the process does not exit. |
| Structured logging | Inspect server logs | The default recover middleware logs the panic with a stack trace; the custom error handler logs returned errors with method, path, timestamp, and request ID. |
| Graceful shutdown | kill -TERM <pid> | Active requests complete, a shutdown message is logged, and the process exits cleanly. |
Limitations and Recovery Options
Background goroutines are not covered. The recover middleware only catches panics on the current request goroutine. If a handler spawns a background goroutine that panics, the process will still crash. Recover inside those goroutines explicitly or use a supervisor pattern.
The error handler must never panic. If your custom ErrorHandler itself panics, the process dies. Keep it simple and avoid nil dereferences.
Version differences. Fiber v1 and v2 differ in middleware and error handler signatures. If the code does not compile, confirm the import path and API shape against the pinned version in go.mod.
Recovery from misconfiguration. If the process still crashes on panic, verify that recover.New() is registered before any routes. If clients see stack traces, confirm the custom error handler is set in fiber.Config and that it returns a generic body. To roll back, remove the deliberate panic route and delete the app.Use(recover.New()) line, then rebuild and redeploy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.