Skip to main content

Chain of Responsibility: Passing Requests Through Handlers

What This Concept Is

Chain of Responsibility (CoR) passes a request along a linked series of handlers. Each handler:

  • decides whether to handle the request
  • optionally produces a result
  • decides whether to forward the request to the next handler

Two common shapes:

  1. Single-handler chain: the first handler that claims the request processes it; others are skipped. Classical CoR, used in GUI event bubbling.
  2. Pipeline (middleware): every handler sees the request, possibly transforms it, and forwards. Used in HTTP middleware, logging filters, validators.

Both are "the same pattern", with a different policy about passing the request forward.

Why It Matters Here

CoR shows up as the underlying shape of:

  • HTTP middleware stacks (Express, ASP.NET, Rack, Koa, Axum)
  • logging filters
  • GUI event dispatch (key presses bubble up through containers)
  • approval workflows
  • request validation pipelines

Recognizing it prevents reinventing its bugs (infinite loops, handlers that forget to forward, order-dependence no one documented).

Concrete Example

A logger chain in TypeScript: debug -> info -> warn -> error. Each handler emits if the level applies and always forwards.

type Level = "debug" | "info" | "warn" | "error";
const order: Level[] = ["debug", "info", "warn", "error"];

interface LogHandler {
handle(level: Level, msg: string): void;
setNext(h: LogHandler): LogHandler;
}

abstract class BaseHandler implements LogHandler {
private next?: LogHandler;
setNext(h: LogHandler) { this.next = h; return h; }
handle(level: Level, msg: string) {
this.emit(level, msg);
this.next?.handle(level, msg);
}
protected abstract emit(level: Level, msg: string): void;
}

class ConsoleHandler extends BaseHandler {
constructor(private min: Level) { super(); }
protected emit(level: Level, msg: string) {
if (order.indexOf(level) >= order.indexOf(this.min)) {
console.log(`[${level}] ${msg}`);
}
}
}

class FileHandler extends BaseHandler {
protected emit(level: Level, msg: string) {
if (level === "error") {
// writeFileSync("errors.log", msg + "\n", { flag: "a" });
}
}
}

class MetricsHandler extends BaseHandler {
private counts: Record<Level, number> = { debug: 0, info: 0, warn: 0, error: 0 };
protected emit(level: Level, msg: string) { this.counts[level]++; }
}

const root = new ConsoleHandler("info");
root.setNext(new FileHandler()).setNext(new MetricsHandler());

root.handle("debug", "starting"); // not printed (below min), but reaches metrics
root.handle("error", "disk full"); // printed, file-logged, counted

Each handler has one job. Reordering or removing a handler is a local change. Adding a new destination is a new class and one wiring line.

Common Confusion / Misconception

  • Forgetting to forward. A middle handler that always handles silently swallows requests. Bugs look like "why didn't that reach X?". Make forwarding the default and not-forwarding an explicit choice.
  • Order dependence with no documentation. If authentication must run before rate limiting, write that in a comment. Production systems fail weirdly when someone reorders the chain innocently.
  • Unbounded chains. A chain that forwards to itself, or a graph built accidentally, loops forever. Test with a finite chain; consider visited-set detection in general-purpose CoR libraries.
  • CoR when a map would do. If "the chain" is really "look up by type and dispatch", a dictionary is clearer. CoR earns its shape when order matters and handlers need to decide.
  • CoR vs Decorator confusion. Same class shape; different intent. Decorators always forward with the same contract; CoR handlers may stop the chain.

How To Use It

  1. Identify the "series of possibly independent checks or transformations" shape.
  2. Define a handler interface with handle(request) and, if needed, setNext(handler).
  3. Implement a BaseHandler that forwards by default; subclasses override only their step.
  4. Build the chain explicitly in client code or via a builder; do not hardcode ordering inside handlers.
  5. Decide on the policy: handle-and-stop, handle-and-forward, or mixed per handler. Document it.
  6. Log entry/exit per handler during development to see what the chain actually did.

Check Yourself

  1. What is the difference between the classical "first handler wins" shape and the pipeline shape?
  2. Why is "forwarding by default" a safer implementation choice?
  3. How is CoR different from a switch over handler type?
  4. When is CoR the wrong pattern, and what replaces it?

Mini Drill or Application

Build a request validation chain for an HTTP endpoint:

  1. Handlers: AuthCheck, RateLimit, PayloadSize, SchemaValidate.
  2. Each may reject (stop the chain with an error) or forward.
  3. Write one test per handler: the request it rejects, the request it lets through.
  4. Prove that reordering two middle handlers is one-line, not a rewrite.

Read This Only If Stuck