Chain of Responsibility in JavaScript Workflows
The Chain of Responsibility is a behavioral design pattern that allows a request to pass through a chain of potential handlers until it is processed or rejected. In JavaScript, this pattern is uniquely suited for managing cascading workflows—such as multi-stage data pipelines, validation sequences, and middleware architectures—by decoupling the sender of a request from its subsequent receivers. This article explores how the pattern functions, how to implement it using modern JavaScript, and how it enables flexible, modular cascading execution.
Core Mechanics of the Pattern
The Chain of Responsibility constructs a sequential pipeline of handler objects or functions. When an operation begins, the initial payload is delivered to the first handler in the chain. Each handler determines whether to: 1. Process the payload completely and terminate the chain. 2. Mutate or enrich the payload and forward it to the next handler. 3. Pass the payload unchanged to the next handler if specific criteria are not met.
This structure eliminates tightly coupled if...else or
switch blocks, allowing developers to add, remove, or
reorder workflow stages without altering surrounding application
logic.
Implementing a Synchronous Cascading Workflow
In a typical cascading workflow, each handler executes a specific business rule and delegates execution down the line. A standard object-oriented approach in modern JavaScript utilizes a base handler class.
class Handler {
setNext(handler) {
this.nextHandler = handler;
return handler; // Enables fluent chaining
}
handle(request) {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return request;
}
}
class AuthenticationHandler extends Handler {
handle(request) {
if (!request.userId) {
throw new Error("Authentication failed: Missing User ID.");
}
request.authenticated = true;
return super.handle(request);
}
}
class ValidationHandler extends Handler {
handle(request) {
if (!request.amount || request.amount <= 0) {
throw new Error("Validation failed: Invalid transaction amount.");
}
request.validated = true;
return super.handle(request);
}
}
class DiscountHandler extends Handler {
handle(request) {
if (request.isVip) {
request.amount *= 0.9; // Apply 10% VIP discount
request.discountApplied = true;
}
return super.handle(request);
}
}Executing the Chain
const auth = new AuthenticationHandler();
const validation = new ValidationHandler();
const discount = new DiscountHandler();
// Assemble the cascade: Auth -> Validation -> Discount
auth.setNext(validation).setNext(discount);
const transaction = { userId: "usr_102", amount: 100, isVip: true };
const processedTransaction = auth.handle(transaction);
console.log(processedTransaction);
// Output: { userId: 'usr_102', amount: 90, isVip: true, authenticated: true, validated: true, discountApplied: true }Handling Asynchronous Cascades
Modern JavaScript workflows frequently involve I/O operations such as
database lookups, third-party API calls, and file system tasks. The
Chain of Responsibility adapts natively to asynchronous execution via
async/await or promise chains.
class AsyncHandler {
setNext(handler) {
this.nextHandler = handler;
return handler;
}
async handle(data) {
if (this.nextHandler) {
return await this.nextHandler.handle(data);
}
return data;
}
}
class FetchUserHandler extends AsyncHandler {
async handle(data) {
// Simulating database lookup
data.user = await Promise.resolve({ id: data.userId, role: "editor" });
return await super.handle(data);
}
}
class PermissionCheckHandler extends AsyncHandler {
async handle(data) {
if (data.user.role !== "admin" && data.action === "DELETE") {
throw new Error("Forbidden: User lacks administrative privileges.");
}
return await super.handle(data);
}
}Functional Chain of Responsibility (Middleware Style)
JavaScript’s support for higher-order functions enables an alternative, functional implementation similar to Express.js or Redux middleware. This approach uses an array of functions composed together with array reduction.
const createPipeline = (...handlers) => (initialInput) => {
return handlers.reduce(
(chain, currentHandler) => chain.then(currentHandler),
Promise.resolve(initialInput)
);
};
// Define individual step functions
const parsePayload = async (data) => ({ ...data, parsed: true });
const sanitizeInput = async (data) => ({ ...data, sanitized: true });
const auditLog = async (data) => {
console.log(`Audited action at: ${new Date().toISOString()}`);
return data;
};
// Compose the workflow
const runWorkflow = createPipeline(parsePayload, sanitizeInput, auditLog);
runWorkflow({ rawInput: "<script>alert(1)</script>" }).then(console.log);Practical Use Cases in JavaScript
- HTTP Middleware: Intercepting incoming web requests to perform logging, session decoding, body parsing, and route handling sequentially.
- Form Validation Pipelines: Running field-by-field validation checks where each handler validates a single constraint (e.g., required, format, uniqueness).
- Data Transformation (ETL): Processing unstructured payloads through normalization, enrichment, and filtering stages before persisting to storage.
- UI Event Bubbling: Propagating events through a hierarchy of DOM elements or custom component wrappers until a targeted listener consumes the event.