# How Caddy Error Handling Works: HandlerError and the handle_errors Directive

> Understand Caddy error handling with HandlerError and the handle_errors directive. Learn how Caddy manages HTTP status codes and custom error routes for robust applications.

- Repository: [Caddy/caddy](https://github.com/caddyserver/caddy)
- Tags: internals
- Published: 2026-03-03

---

**Caddy error handling operates through a structured `HandlerError` type that carries HTTP status codes, unique IDs, and stack traces, combined with the `handle_errors` directive that registers sub-routes executed when specific error status codes are encountered.**

The caddyserver/caddy repository implements a robust error handling mechanism that allows both programmatic error generation in Go handlers and declarative error response configuration via the Caddyfile. This system ensures that HTTP errors are logged, optionally processed through custom handler chains, and returned to clients with appropriate status codes and traceability.

## Core Components of Caddy Error Handling

Caddy's error handling architecture centers on three interconnected components that work together to capture, route, and respond to errors.

### HandlerError Structured Error Type

The **`HandlerError`** struct defined in [`modules/caddyhttp/errors.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/errors.go) serves as the foundation for all HTTP error handling. Unlike standard Go errors, `HandlerError` encapsulates:

- **StatusCode**: The HTTP status code (e.g., 404, 500)
- **ID**: A unique identifier for the error instance
- **Trace**: A stack trace capturing where the error originated
- **Err**: The underlying wrapped error

Handlers use the **`caddyhttp.Error`** helper function to create these structured errors. When invoked, this function automatically generates a random ID and captures the current stack trace, providing observability that standard errors cannot offer.

### The handle_errors Directive

The **`handle_errors`** Caddyfile directive, parsed by the `parseHandleErrors` function in [`caddyconfig/httpcaddyfile/builtins.go`](https://github.com/caddyserver/caddy/blob/main/caddyconfig/httpcaddyfile/builtins.go) (lines 837-909), defines sub-routes that execute only when errors match specific status codes. 

When Caddy parses this directive, it constructs a matcher expression using the format `{http.error.status_code} in [404,410]` and registers the enclosed block as an "error route." These routes are compiled into `s.errorHandlerChain` during server provisioning in [`modules/caddyhttp/app.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/app.go).

### Server Error Flow Orchestration

The **`Server.ServeHTTP`** method in [`modules/caddyhttp/server.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/server.go) (lines 990-1080) orchestrates the complete error handling workflow. This method manages the transition from primary handler execution to error processing, including request state restoration, logging, and fallback response generation.

## The Caddy Error Handling Execution Flow

Understanding the precise sequence of operations helps developers intercept and process errors effectively.

### 1. Error Generation in Handlers

When a handler encounters a failure condition, it returns a structured error using the helper function:

```go
return caddyhttp.Error(http.StatusNotFound, fmt.Errorf("page not found"))

```

This creates a `HandlerError` with a unique ID and stack trace, signaling to the server that an HTTP error response is required.

### 2. Primary Handler Chain Completion

After the compiled primary handler chain executes in `Server.serveHTTP`, any non-nil error triggers the error handling pathway. The server checks if the returned error is a `HandlerError` to extract specific status codes and metadata.

### 3. Request State Restoration

Before invoking error handlers, Caddy restores the original request state. Lines 996-1004 in [`server.go`](https://github.com/caddyserver/caddy/blob/main/server.go) reset the request's method, URL, and remote address to their initial values, ensuring that error handlers work with unmutated request objects even if middleware earlier in the chain modified them.

### 4. Error Context Injection

The server stores the error in the request context using **`ErrorCtxKey`** (defined in [`modules/caddyhttp/errors.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/errors.go)). This makes the error available to downstream modules, including templates that can access error data via `{{ .httpError }}`.

### 5. Structured Logging

The `errLogValues` function extracts the status code, error message, and structured fields from the `HandlerError`, writing them to the configured logger before any response is sent to the client.

### 6. Error Route Execution

If the server configuration includes `handle_errors` blocks, Caddy executes the compiled `s.errorHandlerChain.ServeHTTP`. The matcher expression ensures only errors with matching status codes trigger specific handlers.

If the error route succeeds (returns `nil`), Caddy considers the error handled and logs it without generating a default error response.

### 7. Fallback Response Generation

When no error route exists or the route itself fails, Caddy falls back to default behavior (lines 48-52 in [`server.go`](https://github.com/caddyserver/caddy/blob/main/server.go)):

- If the error is a `HandlerError`, use its `StatusCode`
- Otherwise, return **500 Internal Server Error**

The server writes the final status code header and completes the request.

## Practical Implementation Examples

### Configuring Custom Error Pages in Caddyfile

The `handle_errors` block defines sub-routes for specific status codes:

```caddy
example.com {
    respond "Hello, world!"
}

# Custom error handling for 404 and 410

handle_errors 404 410 {
    file_server {
        root /var/www/errors
    }
}

```

This configuration parses into a matcher expression `{http.error.status_code} in [404,410]` and executes the `file_server` handler only when those errors occur.

### Returning Structured Errors in Go Handlers

Go modules can leverage Caddy's error handling by importing the caddyhttp package:

```go
package main

import (
    "fmt"
    "net/http"

    "github.com/caddyserver/caddy/v2/modules/caddyhttp"
)

func myHandler(w http.ResponseWriter, r *http.Request) error {
    if !authorized(r) {
        return caddyhttp.Error(http.StatusForbidden,
            fmt.Errorf("access denied for %s", r.RemoteAddr))
    }
    w.Write([]byte("OK"))
    return nil
}

```

The `caddyhttp.Error` function populates the `HandlerError.ID` and `HandlerError.Trace` fields automatically, providing debugging information for logs and templates.

### Accessing Error Data in Templates

Templates can render error-specific content by accessing the injected error context:

```html
{{ if .httpError }}
  <h1>Error {{ .httpError.StatusCode }}</h1>
  <p>Trace ID: {{ .httpError.ID }}</p>
  <pre>{{ .httpError.Trace }}</pre>
{{ end }}

```

This functionality is implemented in [`modules/caddyhttp/templates/tplcontext.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/templates/tplcontext.go) via the `TemplateContext.funcHTTPError` method, which retrieves the error from the request context using `ErrorCtxKey`.

## Key Source Files and Functions

The following files in the caddyserver/caddy repository implement the error handling system:

- **[`modules/caddyhttp/errors.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/errors.go)**: Defines `HandlerError`, the `Error` helper function, and `ErrorCtxKey` for context storage.
- **[`caddyconfig/httpcaddyfile/builtins.go`](https://github.com/caddyserver/caddy/blob/main/caddyconfig/httpcaddyfile/builtins.go)** (lines 837-909): Contains `parseHandleErrors`, which parses the `handle_errors` directive and builds matcher expressions.
- **[`modules/caddyhttp/server.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/server.go)** (lines 990-1080): Implements `ServeHTTP` with the core error handling flow, including request restoration (lines 996-1004) and fallback response logic (lines 48-52).
- **[`modules/caddyhttp/app.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/app.go)**: Pre-compiles error handler chains during server provisioning.
- **[`modules/caddyhttp/templates/tplcontext.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/templates/tplcontext.go)** (lines 450-470): Exposes error data to templates through `{{ .httpError }}`.

## Summary

- **HandlerError** provides structured error information including HTTP status codes, unique IDs, and stack traces, defined in [`modules/caddyhttp/errors.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/errors.go).
- The **`handle_errors`** directive creates matcher-driven sub-routes that execute when specific error status codes occur, parsed in [`caddyconfig/httpcaddyfile/builtins.go`](https://github.com/caddyserver/caddy/blob/main/caddyconfig/httpcaddyfile/builtins.go).
- Caddy restores the original request state before executing error handlers to ensure consistent processing.
- Errors are injected into the request context via `ErrorCtxKey`, making them accessible to templates and other modules.
- The error handling flow prioritizes user-defined routes, falling back to default status codes only when routes fail or are undefined.

## Frequently Asked Questions

### How do I create a custom 404 page in Caddy?

Use the `handle_errors` directive in your Caddyfile to define a sub-route that serves static files when 404 errors occur. For example, `handle_errors 404 { file_server { root /var/www/errors } }` will serve content from `/var/www/errors` when a 404 status is triggered. The request state is automatically restored to its original condition before the error handler executes.

### What information does Caddy include in structured errors?

Caddy's `HandlerError` struct includes the HTTP status code, a unique random ID for tracing, a complete stack trace captured at the error creation point, and the underlying error message. When using the `caddyhttp.Error` helper, the ID and trace are populated automatically, providing observability for debugging distributed requests.

### Can I handle multiple HTTP error codes with a single handler?

Yes, the `handle_errors` directive accepts multiple status codes as arguments, such as `handle_errors 404 410 500 { ... }`. The Caddyfile parser builds a matcher expression using `{http.error.status_code} in [404,410,500]`, causing the enclosed handler chain to execute for any of the specified status codes.

### How can I access error details in Caddy templates?

Error details are available in templates through the `{{ .httpError }}` object, which exposes the `StatusCode`, `ID`, and `Trace` fields. This data is injected into the request context under `ErrorCtxKey` by the server error handling logic in [`modules/caddyhttp/server.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/server.go), allowing templates to render error-specific debugging information or custom error pages.