---
title: "GN009 – Add gin.Recovery() Middleware in Production"
impact: high
impactDescription: "Without Recovery(), an unhandled panic terminates the goroutine and returns a broken TCP connection or empty response to the client; with it, the server stays up and returns 500."
tags: [go, gin, reliability, middleware]
---

# GN009 – Add `gin.Recovery()` Middleware in Production

## Rule

Always include `gin.Recovery()` (or a custom recovery middleware) in the production middleware stack. Use `gin.New()` rather than `gin.Default()` and add middleware explicitly so you control what runs.

## Why

Go panics propagate up the call stack and terminate the goroutine. In a Gin HTTP handler, an unrecovered panic kills the request-handling goroutine and the server returns a broken connection. `gin.Recovery()` catches the panic, logs a stack trace, and returns `500 Internal Server Error`, keeping the server alive.

## Wrong

```go
func main() {
    r := gin.New()   // ❌ no Recovery — a panic crashes the goroutine silently
    r.GET("/users/:id", userHandler.Get)
    r.Run(":8080")
}

// gin.Default() adds Logger + Recovery but also adds noise to structured log setups
r := gin.Default()  // acceptable but not recommended for production structured logging
```

## Correct

```go
func main() {
    gin.SetMode(os.Getenv("GIN_MODE"))  // see GN007

    r := gin.New()

    // Custom structured logger (replace gin.Logger())
    r.Use(RequestLogger(logger))

    // Recovery — MUST be registered before route handlers
    r.Use(gin.RecoveryWithWriter(gin.DefaultErrorWriter))
    // OR a custom recovery that logs stack traces to your logger:
    r.Use(CustomRecovery(logger))

    registerRoutes(r)
    r.Run(":" + cfg.Port)
}

// Custom recovery middleware with structured logging
func CustomRecovery(logger *slog.Logger) gin.HandlerFunc {
    return gin.CustomRecoveryWithWriter(gin.DefaultErrorWriter, func(c *gin.Context, recovered any) {
        logger.Error("panic recovered",
            slog.Any("error", recovered),
            slog.String("path", c.Request.URL.Path),
            slog.String("method", c.Request.Method),
        )
        c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
    })
}
```

## Notes

- Recovery middleware must be registered **before** route handlers — Gin runs middleware in registration order.
- Don't expose the panic value or stack trace in the HTTP response — log it server-side only.
- For observability, send panic events to your error tracker (Sentry, Rollbar) inside the custom recovery handler.
