Chain of Responsibility Pattern in JavaScript for building flexible pipelines for real-World architectures.
Chain of Responsibility lets us pass a request along a chain of handlers where each handler decides:
- To handle it (stop the chain)
- Transform it and forward (continue)
- Ignore and forward (pass responsibility further)
The client doesn’t need to know who handled the request — it just fires and forgets, this is like a relay race: the baton (request) keeps moving until someone crosses the finish line (a handler processes it).
Why does CoR even matter
Chain of responsibility is useful in;
- Pluggable architectures – middleware, pipelines, and workflows.
- Evolving requirements – adding a new handler doesn’t break existing code. You extend the chain instead of rewriting conditionals.
- Decoupling – clients don’t care how a request is handled; they just start the chain.
1. Example of CoR in Javascript
- Base handler Here we create
Handler
class that defines the contract for every participant in the chain. It has two responsibilities: (1) allow chaining through setNext, and (2) pass requests forward if it cannot handle them. By storing thenextHandler
reference internally, each handler acts like a linked list node, pointing to the next element in the chain. The return ofsetNext(handler)
enables fluent chaining, making pipeline assembly both concise and expressive. Thehandle(request)
method is the backbone of delegation: if the current handler doesn’t take action, it checks if a successor exists and forwards the request, otherwise the chain ends with null. This design keeps the client ignorant of the internal chain structure while ensuring handlers can evolve independently.
class Handler {
setNext(handler) {
this.nextHandler = handler;
return handler; // enables fluent chaining
}
handle(request) {
if (this.nextHandler) {
return this.nextHandler.handle(request); // delegate to next
}
return null; // no handler could process
}
}
- Concrete handlers Here we will define three concrete handlers—
AuthHandler, LoggingHandler, and PaymentHandler
—that extend abase Handler class
in a Chain of Responsibility setup. Each handler checks whether it is responsible for the incoming request by examining its type. For example,AuthHandler
processes requests of type "auth",LoggingHandler
handles "log", andPaymentHandler
handles "payment". If a handler cannot process the request, it callssuper.handle(request)
, which delegates the request to the next handler in the chain. This design keeps the logic modular: each handler is focused on a single responsibility, and the chain as a whole can grow or shrink without changing client code.
class AuthHandler extends Handler {
handle(request) {
if (request.type === 'auth') {
console.log('AuthHandler processed the request');
return true;
}
return super.handle(request); // delegate if not responsible
}
}
class LoggingHandler extends Handler {
handle(request) {
if (request.type === 'log') {
console.log('LoggingHandler recorded a log');
return true;
}
return super.handle(request);
}
}
class PaymentHandler extends Handler {
handle(request) {
if (request.type === 'payment') {
console.log('PaymentHandler processed payment');
return true;
}
return super.handle(request);
}
}
- Chain assembly Here we create three handlers—
AuthHandler, LoggingHandler, and PaymentHandler
—each responsible for a different type of request. We then link them together usingsetNext()
, forming a chain whereauth
passes tologger
, which in turn passes topayment
. When we send a request into the chain by callingauth.handle()
, each handler checks if it can process the request. If it can, it handles it; if not, it forwards the request to thenext handler
. This allows different requests (auth, log, payment) to be handled by the right handler without the client knowing who is responsible, while unrecognized requests (like email) simply fall off the chain unhandled.
// Create handlers
const auth = new AuthHandler();
const logger = new LoggingHandler();
const payment = new PaymentHandler();
// Assemble chain: auth → logger → payment
auth.setNext(logger).setNext(payment);
// Send requests into the chain
auth.handle({ type: 'auth' }); // handled by AuthHandler
auth.handle({ type: 'log' }); // passed to LoggingHandler
auth.handle({ type: 'payment' }); // passed to PaymentHandler
auth.handle({ type: 'email' }); // null - unhandled
2. CoR used in Express.js middleware
In express each app.use
call registers a handler
that receives the request (req), the response (res), and a next function
used to pass control to the next
handler in the chain. The first middleware logs every incoming request, then calls next()
to delegate control. The second middleware acts like an authentication guard: if the request has no user property, it immediately returns a 401 Unauthorized response; otherwise, it calls next()
to continue. Finally, if the request passes through the chain successfully, the /dashboard
route handler sends back “Dashboard content.” The key idea is that requests move through a pipeline of handlers, where each one can act, block, or delegate responsibility without the client needing to know the internal flow.
app.use((req, res, next) => {
console.log('Middleware 1: Logging');
next(); // delegate responsibility
});
app.use((req, res, next) => {
if (!req.user) return res.status(401).send('Unauthorized');
next(); // continue only if authenticated
});
app.get('/dashboard', (req, res) => {
res.send('Dashboard content');
});
3. Let us use CoR in a document approval app
In the code below we demonstrate the Chain of Responsibility pattern through a document approval workflow. Each class (ManagerApproval, DirectorApproval, CEOApproval) extends a base Handler and implements a handle(doc) method. When a request (a document with an amount) is passed into the chain, the manager first checks: if the amount is below 1000, it approves and stops there; otherwise, it delegates upward by calling super.handle(doc)
, which forwards the request to the next handler in the chain. The director then checks for amounts below 10,000, and if not satisfied, escalates further to the CEO. The CEO is the final authority, approving any request that reaches them. The client code creates a chain (Manager - Director - CEO) and simply calls manager.handle(doc)
without caring who will approve it. For example, an amount of 500 is approved at the manager level, 5000 by the director, and 50,000 by the CEO. This neatly models a real-world escalation process while keeping each role’s logic isolated and easily extendable.
// Abstract handler defines the chain behavior
class Handler {
setNext(handler) {
this.nextHandler = handler;
return handler; // fluent chaining
}
handle(request) {
if (this.nextHandler) {
return this.nextHandler.handle(request); // delegate to next handler
}
return null; // nobody could handle it
}
}
class ManagerApproval extends Handler {
handle(doc) {
if (doc.amount < 1000) {
console.log('Manager approved ');
return true;
}
return super.handle(doc);
}
}
class DirectorApproval extends Handler {
handle(doc) {
if (doc.amount < 10000) {
console.log('Director approved ');
return true;
}
return super.handle(doc);
}
}
class CEOApproval extends Handler {
handle(doc) {
console.log('CEO approved');
return true;
}
}
// Chain: Manager - Director - CEO
const manager = new ManagerApproval();
manager.setNext(new DirectorApproval()).setNext(new CEOApproval());
manager.handle({ amount: 500 }); // Manager
manager.handle({ amount: 5000 }); // Director
manager.handle({ amount: 50000 }); // CEO
Avoid CoR if
You have 1–2 conditions — an if statement is simpler. If Performance is critical, and passing through multiple handlers adds overhead.
javascript book
If this interested you, check out my Javascript Book