Back to JavaScript
JavaScript
easy
mid

How would you implement a chainable Driver class in JavaScript?

Method chaining with deferred async actions: each method enqueues an operation and returns `this` synchronously; an internal promise chain awaits each step. Common interview ask: `driver.start().drive(10).stop().wait(5).honk()` — each call appended to a queue, executed in order, with errors propagating cleanly.

4 min read·~25 min to think through

The "chainable driver class" interview asks you to build the deferred-async chaining pattern behind tools like Cypress, Puppeteer, or the classic "ninja eats sleeps wakes" puzzle.

The challenge

Calls like driver.start().drive(10).stop() look synchronous but the operations are async and ordered. The trick: return this immediately, but internally chain promises so each operation waits for the previous.

The implementation

js
class Driver {
  constructor() {
    this._chain = Promise.resolve();
  }

  _enqueue(fn) {
    this._chain = this._chain.then(fn);
    return this;
  }

  start() {
    return this._enqueue(async () => {
      console.log("Started");
    });
  }

  drive(seconds) {
    return this._enqueue(async () => {
      console.log(`Driving for ${seconds}s`);
      await new Promise((r) => setTimeout(r, seconds * 1000));
    });
  }

  wait(seconds) {
    return this._enqueue(() => new Promise((r) => setTimeout(r, seconds * 1000)));
  }

  stop() {
    return this._enqueue(async () => console.log("Stopped"));
  }

  honk() {
    return this._enqueue(async () => console.log("Honk!"));
  }

  // optional: make it awaitable
  then(onFulfilled, onRejected) {
    return this._chain.then(onFulfilled, onRejected);
  }
}

Usage

js
const d = new Driver();
d.start().drive(2).honk().wait(1).stop();

// or, awaitable thanks to `then`:
await new Driver().start().drive(2).stop();

What makes this work

  1. Every method returns this synchronously → enables chaining without awaits.
  2. Internally, each method appends to a promise chain → operations run in order.
  3. then on the instanceawait driver.start().... works.

Variant: explicit schedule puzzle

Classic ninja puzzle:

js
new Ninja("Hattori").eat("rice").sleepFor(3).learn("nunchuck");

Same pattern: each call queues an op, the queue executes serially.

js
class Ninja {
  constructor(name) {
    this.name = name;
    this._chain = Promise.resolve();
  }
  _q(fn) { this._chain = this._chain.then(fn); return this; }
  eat(food) { return this._q(() => console.log(`${this.name} eats ${food}`)); }
  sleepFor(s) { return this._q(() => new Promise((r) => setTimeout(() => { console.log(`${this.name} woke up`); r(); }, s * 1000))); }
  learn(skill) { return this._q(() => console.log(`${this.name} learns ${skill}`)); }
}

Error handling

Errors propagate down the chain:

js
class Driver {
  // ...
  _enqueue(fn) {
    this._chain = this._chain.then(fn);
    this._chain.catch((err) => console.error("Driver error:", err));  // tail catch
    return this;
  }
}

Or expose .catch:

js
catch(handler) { return this._chain.catch(handler); }

Sleep-then-action pattern (the "ninja" twist)

A common puzzle: sleepFor(3) must delay subsequent calls too. The above implementation handles that — because subsequent calls await the chain, which is now awaiting the timer.

A trickier variant inserts a sleepFirst(3) that delays prior calls — which requires queueing the entire chain with the sleep prepended. That's an architectural change (queue with insertion-at-head) and worth flagging.

Cancellation

Add an abort() that rejects the next then:

js
abort() { this._chain = Promise.reject(new Error("aborted")); }

Interview framing

"The trick is: every method returns this synchronously, but internally we maintain a promise chain — each method appends .then(fn) onto this._chain. So driver.start().drive(2).stop() synchronously queues three operations; the chain executes them in order, awaiting any async (like a timer in drive). Implement then on the instance so the whole driver is awaitable. Errors propagate down the chain — expose .catch or attach a tail handler. The 'sleepFirst' variant — sleeping before earlier queued items — requires queueing structurally rather than a flat chain."

Follow-up questions

  • How does `then` on the instance make it awaitable?
  • How would you implement `sleepFirst(s)` that delays prior calls?
  • How do you propagate errors cleanly?
  • Where does this pattern appear in real libraries?

Common mistakes

  • Returning a promise instead of `this` — kills chaining.
  • Awaiting inside methods synchronously (await this._chain) — but then return value is wrong.
  • No error handler — silent failures.
  • Forgetting `then` — can't await the chain.

Performance considerations

  • One microtask per queued op. Trivial cost.

Edge cases

  • Errors mid-chain — should later steps still run? (typically no.)
  • Re-awaiting the same instance.
  • Mixing sync and async methods.

Real-world examples

  • Cypress commands (cy.visit().get().click() — same pattern).
  • Puppeteer page actions.
  • jQuery's chainable API (synchronous but the same shape).

Senior engineer discussion

Seniors recognize this as the deferred-async chaining pattern used in test frameworks, get the `then` on instance trick right for awaitability, and handle errors deliberately. They distinguish the 'sleepFirst' variant as a structural change, not a one-line tweak.

Related questions