# How to Implement Custom Middleware with Error Handling in Gin

> Learn to implement custom middleware with error handling in Gin. Centralize error logging and JSON responses by inspecting cErrors after the chain returns. Build robust web applications.

- Repository: [Gin-Gonic/gin](https://github.com/gin-gonic/gin)
- Tags: how-to-guide
- Published: 2026-02-27

---

**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/main/gin.go)](https://github.com/gin-gonic/gin/blob/master/gin.go#L51). This signature applies equally to route handlers and middleware:

```go
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/main/routergroup.go)](https://github.com/gin-gonic/gin/blob/master/routergroup.go#L64-L66):

```go
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/main/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:

```go
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:

```go
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()`:

```go
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/main/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

- **Middleware signature**: Implement `func(*gin.Context)` to create compatible middleware for the `gin-gonic/gin` router
- **Registration**: Use `router.Use()` in [[`routergroup.go`](https://github.com/gin-gonic/gin/blob/main/routergroup.go)](https://github.com/gin-gonic/gin/blob/master/routergroup.go) to attach middleware to specific groups or the entire engine
- **Error capture**: Call `c.Next()` to execute downstream handlers, then check `len(c.Errors)` after it returns
- **Abort semantics**: Use `c.AbortWithError(code, err)` in route handlers to stop processing and record the error, implemented in [[`context.go`](https://github.com/gin-gonic/gin/blob/main/context.go)](https://github.com/gin-gonic/gin/blob/master/context.go)
- **Panic safety**: Include `gin.Recovery()` from [[`recovery.go`](https://github.com/gin-gonic/gin/blob/main/recovery.go)](https://github.com/gin-gonic/gin/blob/master/recovery.go) to convert panics into HTTP 500 responses without crashing the server

## 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`](https://github.com/gin-gonic/gin/blob/main/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/main/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.