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