How Caddy Error Handling Works: HandlerError and the handle_errors Directive

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 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 (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.

Server Error Flow Orchestration

The Server.ServeHTTP method in 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:

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 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). 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):

  • 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:

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:

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:

{{ 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 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:

Summary

  • HandlerError provides structured error information including HTTP status codes, unique IDs, and stack traces, defined in 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.
  • 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, allowing templates to render error-specific debugging information or custom error pages.

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 →