Skip to main content

Recovery Middleware

The Recovery middleware catches panics in your application, prevents server crashes, and gracefully passes errors to the centralized error handler.

Usage

Basic Usage

import (
"go-slim.dev/slim"
"go-slim.dev/slim/middleware"
)

func main() {
s := slim.New()

// Recovery should come after Logger
s.Use(middleware.Logger())
s.Use(middleware.Recovery())

s.GET("/panic", func(c slim.Context) error {
panic("something went wrong!")
})

s.Start(":8080")
}

Custom Configuration

s := slim.New()

s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
StackSize: 4 << 10, // 4 KB
DisableStackAll: false,
DisablePrintStack: false,
}))

Configuration Options

RecoveryConfig

type RecoveryConfig struct {
// StackSize defines the size of the stack to be printed
// Optional. Default value 4KB
StackSize int

// DisableStackAll disables formatting stack traces of all other goroutines
// Optional. Default value false
DisableStackAll bool

// DisablePrintStack disables printing stack trace
// Optional. Default value false
DisablePrintStack bool
}

Examples

1. Basic Panic Recovery

s := slim.New()

s.Use(middleware.Logger())
s.Use(middleware.Recovery())

s.GET("/panic", func(c slim.Context) error {
panic("unexpected error!")
})

s.Start(":8080")

When accessing /panic:

  • Recovery middleware catches the panic
  • Prints colored stack trace to stderr
  • Returns 500 Internal Server Error
  • Server continues running

2. Disable Stack Printing

s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
DisablePrintStack: true, // Don't print stack info
}))

Suitable for production environments to avoid leaking sensitive information.

3. Print Only Current Goroutine Stack

s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
DisableStackAll: true, // Only print current goroutine stack
}))

Reduces log output and improves readability.

4. Custom Stack Size

s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
StackSize: 8 << 10, // 8 KB for deeper call stacks
}))

5. Integration with Logger Middleware

s := slim.New()

s.ErrorHandler = func(c slim.Context, err error) error {
middleware.LogEnd(c, err)

// Custom error response
return c.JSON(500, map[string]string{
"error": "Internal Server Error",
})
}

// Logger should come before Recovery
s.Use(middleware.Logger())
s.Use(middleware.Recovery())

s.GET("/panic", func(c slim.Context) error {
panic("oops!")
})

Output example:

2024/11/08 10:30:15.123 "GET /panic HTTP/1.1" from 127.0.0.1:54321 - 500 35B in 1.2ms

panic: oops!

-> github.com/your/project/handler.PanicHandler
/path/to/handler.go:123
main.main
/path/to/main.go:45

6. Handle Specific Panic Types

s.Use(middleware.Recovery())

s.Use(func(c slim.Context, next slim.HandlerFunc) error {
defer func() {
if r := recover(); r != nil {
// Handle specific panic types
if err, ok := r.(error); ok {
c.Error(err)
return
}
// Re-throw other panics
panic(r)
}
}()
return next(c)
})

Stack Trace Format

Recovery middleware provides beautified stack trace output:

Colored Output (TTY)

 panic: something went wrong!

-> go-slim.dev/slim/middleware.panicHandler
/path/to/middleware.go:123
go-slim.dev/slim.(*Context).Next
/path/to/context.go:456
main.main
/path/to/main.go:789

Color legend:

  • Red arrow (->): Where the panic occurred
  • Purple: Package name
  • Red: Function name (panic location)
  • Green: Function name (other locations)
  • Cyan: File name
  • Green: Line number

Plain Text Output (Non-TTY)

In non-TTY environments (e.g., log files), output does not include color codes.

How It Works

  1. Catch Panic: Uses defer recover() to catch panics in the handler chain
  2. Collect Stack: Calls runtime.Stack() to get stack trace
  3. Format Output: Beautifies stack information and outputs to log
  4. Set Response:
    • If response not written, sets 500 status code
    • If WebSocket upgrade, does not modify response
  5. Continue Running: Server continues processing other requests

Middleware Order

Important: Recovery should come after Logger to ensure panics are properly logged:

s.ErrorHandler = func(c slim.Context, err error) error {
middleware.LogEnd(c, err) // Log error
return err
}

s.Use(middleware.Logger()) // Logger first
s.Use(middleware.Recovery()) // Recovery after

Panics Not Recovered

Recovery middleware will not recover the following panics:

http.ErrAbortHandler

s.GET("/abort", func(c slim.Context) error {
panic(http.ErrAbortHandler) // Will not be recovered
})

http.ErrAbortHandler is a special panic used to abort responses. Recovery will re-throw it.

Best Practices

1. Always Use Recovery Middleware

s := slim.New()

// After all other middleware (except Logger)
s.Use(middleware.Logger())
s.Use(middleware.Recovery())

2. Production Environment Configuration

func newRecoveryMiddleware() slim.MiddlewareFunc {
if os.Getenv("ENV") == "production" {
return middleware.RecoveryWithConfig(middleware.RecoveryConfig{
DisablePrintStack: true, // Don't print stack in production
})
}
return middleware.Recovery() // Print full stack in development
}

s.Use(newRecoveryMiddleware())

3. Centralized Error Handling

s.ErrorHandler = func(c slim.Context, err error) error {
middleware.LogEnd(c, err)

// Differentiate error types
if he, ok := err.(*slim.HTTPError); ok {
return c.JSON(he.Code, he)
}

// Hide internal error details
return c.JSON(500, map[string]string{
"error": "Internal Server Error",
"id": c.Header(slim.HeaderXRequestID),
})
}

4. Log Panics to Monitoring Systems

type customLogEntry struct {
logger *slog.Logger
}

func (c *customLogEntry) Panic(v any, stack []byte) {
// Log to logging system
c.logger.Error("panic recovered",
slog.Any("panic", v),
slog.String("stack", string(stack)))

// Send to monitoring system (e.g., Sentry)
sentry.CaptureException(fmt.Errorf("panic: %v", v))

// Print to stderr
middleware.PrintPrettyStack(v, stack)
}

s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
NewEntry: func(c slim.Context) middleware.LogEntry {
return &customLogEntry{
logger: slog.Default(),
}
},
}))

s.Use(middleware.Recovery())

5. Graceful Degradation

s.GET("/api/data", func(c slim.Context) error {
defer func() {
if r := recover(); r != nil {
// Log error
log.Printf("Error processing request: %v", r)

// Return cached data or default value
c.JSON(200, getCachedData())
}
}()

// Code that might panic
data := fetchDataFromExternalAPI()
return c.JSON(200, data)
})

Integration with Other Monitoring Tools

Integration with Sentry

import (
"github.com/getsentry/sentry-go"
"go-slim.dev/slim/middleware"
)

func init() {
sentry.Init(sentry.ClientOptions{
Dsn: os.Getenv("SENTRY_DSN"),
})
}

s.Use(func(c slim.Context, next slim.HandlerFunc) error {
defer func() {
if r := recover(); r != nil {
// Send to Sentry
sentry.CaptureException(fmt.Errorf("panic: %v", r))

// Re-throw to let Recovery middleware handle
panic(r)
}
}()
return next(c)
})

s.Use(middleware.Recovery())

Custom Panic Handler

func customRecovery() slim.MiddlewareFunc {
return func(c slim.Context, next slim.HandlerFunc) error {
defer func() {
if r := recover(); r != nil {
// Get stack
stack := make([]byte, 4<<10)
n := runtime.Stack(stack, false)
stack = stack[:n]

// Log
log.Printf("PANIC: %v\n%s", r, stack)

// Send alert
sendAlert(fmt.Sprintf("Panic: %v", r))

// Return error response
if !c.Response().Written() {
c.JSON(500, map[string]string{
"error": "Internal Server Error",
})
}
}
}()
return next(c)
}
}

s.Use(customRecovery())

Testing Recovery Middleware

func TestRecovery(t *testing.T) {
s := slim.New()
s.Use(middleware.Recovery())

s.GET("/panic", func(c slim.Context) error {
panic("test panic")
})

req := httptest.NewRequest("GET", "/panic", nil)
rec := httptest.NewRecorder()

s.ServeHTTP(rec, req)

// Verify response status code
if rec.Code != 500 {
t.Errorf("expected 500, got %d", rec.Code)
}
}

Default Configuration

DefaultRecoveryConfig = RecoveryConfig{
StackSize: 4 << 10, // 4 KB
DisableStackAll: false,
DisablePrintStack: false,
}

Performance Considerations

  1. Stack Collection: Collecting stack information has some overhead, but only triggers on panic
  2. Stack Size: Default 4KB is sufficient for most cases, can be increased for deep call stacks
  3. Log Output: Beautifying output requires parsing stack, but doesn't significantly impact performance

Security Considerations

  1. Don't expose stack information in production:

    s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
    DisablePrintStack: true, // Production
    }))
  2. Avoid leaking sensitive information:

    s.ErrorHandler = func(c slim.Context, err error) error {
    middleware.LogEnd(c, err)

    // Don't return error details directly
    return c.JSON(500, map[string]string{
    "error": "Internal Server Error",
    })
    }

Common Questions

Q: Can Recovery catch panics in async code?

A: No. Recovery can only catch panics in the current goroutine. Async code needs separate handling:

s.GET("/async", func(c slim.Context) error {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in goroutine: %v", r)
}
}()
// Async code
panic("async panic")
}()
return c.String(200, "OK")
})

Q: Why should Recovery come after Logger?

A: This ensures panics are properly logged. If Recovery comes before Logger, Logger may not be able to log panic information.

Q: Can I customize panic responses?

A: Yes. Recovery only catches the panic and sets a 500 status code. The actual response content is determined by the error handler:

s.ErrorHandler = func(c slim.Context, err error) error {
return c.JSON(500, customErrorResponse)
}

References