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:
- Single-handler chain: the first handler that claims the request processes it; others are skipped. Classical CoR, used in GUI event bubbling.
- 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
- Identify the "series of possibly independent checks or transformations" shape.
- Define a handler interface with
handle(request)and, if needed,setNext(handler). - Implement a
BaseHandlerthat forwards by default; subclasses override only their step. - Build the chain explicitly in client code or via a builder; do not hardcode ordering inside handlers.
- Decide on the policy: handle-and-stop, handle-and-forward, or mixed per handler. Document it.
- Log entry/exit per handler during development to see what the chain actually did.
Check Yourself
- What is the difference between the classical "first handler wins" shape and the pipeline shape?
- Why is "forwarding by default" a safer implementation choice?
- How is CoR different from a
switchover handler type? - When is CoR the wrong pattern, and what replaces it?
Mini Drill or Application
Build a request validation chain for an HTTP endpoint:
- Handlers:
AuthCheck,RateLimit,PayloadSize,SchemaValidate. - Each may reject (stop the chain with an error) or forward.
- Write one test per handler: the request it rejects, the request it lets through.
- Prove that reordering two middle handlers is one-line, not a rewrite.