How to Implement Custom Middleware with Error Handling in Gin

Gin middleware are HandlerFunc types that execute c.Next() to pass control downstream, then inspect c.Errors after the chain returns to centralize error logging and JSON responses.

Gin’s request processing pipeline in the gin-gonic/gin repository is built around a sequential chain of handler functions that process HTTP requests. Implementing custom middleware with error handling allows you to capture errors from downstream handlers using AbortWithError, format consistent JSON payloads, and log issues without duplicating code across individual routes.

Understanding Gin's Middleware Architecture

The HandlerFunc Type

At the core of Gin’s middleware system lies the HandlerFunc type defined in [gin.go](https://github.com/gin-gonic/gin/blob/master/gin.go#L51). This signature applies equally to route handlers and middleware:

type HandlerFunc func(*Context)

Every middleware you write must conform to this signature, accepting a pointer to the Gin context which provides access to the request, response writer, and error collection.

Registering Middleware with Use

Middleware registration occurs through the Use method implemented in [routergroup.go](https://github.com/gin-gonic/gin/blob/master/routergroup.go#L64-L66):

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

When processing a request, Gin constructs a handler chain consisting of:

  1. Global middleware attached to the root engine
  2. Group-specific middleware attached via RouterGroup
  3. The final route handler

The chain executes in strict registration order, allowing each middleware to perform work before, during, and after downstream handlers run.

How Error Handling Works in Gin

Stopping the Handler Chain

Gin provides two methods to halt middleware execution in [context.go](https://github.com/gin-gonic/gin/blob/master/context.go):

  • c.Abort() – Immediately stops the chain without recording an error
  • c.AbortWithError(code, err) – Stops the chain and appends the error to c.Errors

The AbortWithError implementation reveals how Gin tracks errors internally:

func (c *Context) AbortWithError(code int, err error) *Error {
    c.AbortWithStatus(code)
    return c.Error(err)
}

The Next() Pattern for Post-Processing

The key architectural pattern for error handling middleware is deferred processing:

  1. Call c.Next() to execute all downstream handlers
  2. After Next() returns, inspect c.Errors to check if any handler called AbortWithError
  3. If errors exist, format and write the response

This pattern ensures your middleware sees the complete result of the request handling, including any errors accumulated during execution.

Implementing a Custom Error Handling Middleware

A production-ready error handling middleware captures errors after Next() completes and returns a standardized JSON response. This implementation consolidates error formatting and logging in one location:

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        // Execute downstream handlers
        c.Next()

        // Check if any errors were accumulated
        if len(c.Errors) > 0 {
            // Retrieve the first error (or iterate for multiple)
            err := c.Errors[0]
            
            // Determine appropriate status code
            status := c.Writer.Status()
            if status < 400 {
                status = http.StatusInternalServerError
            }

            // Log the error (in production, use structured logging)
            log.Printf("Request error: %v (status %d)", err.Err, status)

            // Return consistent JSON error payload
            c.JSON(status, gin.H{
                "error":   err.Err.Error(),
                "code":    status,
                "message": err.Meta, // optional metadata
            })
        }
    }
}

Complete Integration Example

Wire together logging, error handling, and panic recovery to create a robust middleware stack. The following example demonstrates how errors flow through the chain when using gin.New() (which excludes default middleware) instead of gin.Default():

package main

import (
    "fmt"
    "net/http"
    "os"

    "github.com/gin-gonic/gin"
)

// Logger middleware prints request details before continuing
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        fmt.Printf("[GIN] %s %s\n", c.Request.Method, c.Request.URL.Path)
        c.Next()
    }
}

// ErrorHandler captures AbortWithError calls and returns JSON
func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        if len(c.Errors) > 0 {
            err := c.Errors[0]
            status := c.Writer.Status()
            if status < 400 {
                status = http.StatusInternalServerError
            }
            
            fmt.Fprintf(os.Stderr, "error: %v (status %d)\n", err.Err, status)
            
            c.JSON(status, gin.H{
                "error": err.Err.Error(),
                "code":  status,
            })
        }
    }
}

func main() {
    // Create engine without default middleware
    r := gin.New()
    
    // Register middleware in execution order
    r.Use(Logger())        // Request logging
    r.Use(ErrorHandler())  // Error processing
    r.Use(gin.Recovery())  // Panic recovery (from recovery.go)

    // Route that triggers an error
    r.GET("/auth", func(c *gin.Context) {
        c.AbortWithError(http.StatusUnauthorized, 
            fmt.Errorf("authentication required"))
        return
    })

    // Route that causes a panic (caught by Recovery)
    r.GET("/panic", func(c *gin.Context) {
        panic("unexpected error")
    })

    r.Run(":8080")
}

When a request hits /auth, the execution flow follows this path:

  1. Logger prints the request method and path
  2. ErrorHandler calls c.Next(), allowing the /auth handler to execute
  3. The handler calls c.AbortWithError(401, ...), which stops further processing and appends the error
  4. Control returns to ErrorHandler, which detects c.Errors and returns a JSON 401 response

Handling Panics with Recovery

Gin ships with built-in panic recovery middleware defined in [recovery.go](https://github.com/gin-gonic/gin/blob/master/recovery.go). When combined with your custom error handler, Recovery catches panics before they crash the application:

  • gin.Recovery() – Returns a generic 500 response
  • gin.CustomRecovery(handle gin.RecoveryFunc) – Allows custom panic handling logic

Place Recovery after your error handler in the middleware stack if you want your error handler to process the recovery response, or before it if you want separate handling for panics versus business logic errors.

Summary

Frequently Asked Questions

What is the difference between Abort and AbortWithError?

c.Abort() stops the middleware chain immediately without recording an error in c.Errors, useful when you want to halt processing but handle the response manually. c.AbortWithError(code, err) also stops the chain but appends the supplied error to c.Errors and sets the HTTP status code, allowing upstream error handling middleware to detect and format the failure automatically according to the implementation in context.go.

How do I access errors after calling Next() in Gin middleware?

After calling c.Next(), inspect the c.Errors slice which contains *Error instances populated by c.AbortWithError() calls from downstream handlers. Each error provides access to the original error, HTTP status code, and optional metadata via err.Err, err.Type, and err.Meta fields defined in the Gin context package.

Can I register error handling middleware for specific route groups only?

Yes, call group.Use(ErrorHandler()) on a specific RouterGroup instead of the root engine. Middleware registered on a group only executes for routes within that group prefix, allowing you to apply different error handling strategies (such as HTML error pages for web routes versus JSON for API routes) based on the route organization in [routergroup.go](https://github.com/gin-gonic/gin/blob/master/routergroup.go).

How does Recovery middleware interact with custom error handlers?

gin.Recovery() catches panics and writes a 500 status response before your error handler runs. If you place Recovery before your error handler in the middleware stack, your error handler will see the 500 status in c.Writer.Status() but typically won't see entries in c.Errors because Recovery doesn't use AbortWithError. Place Recovery after your error handler if you want to handle panics differently from business logic errors, or use gin.CustomRecovery to integrate panic handling with your error collection system.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →