Implement a Chainable Driver class
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.
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
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
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
- Every method returns
thissynchronously → enables chaining without awaits. - Internally, each method appends to a promise chain → operations run in order.
thenon the instance →await driver.start()....works.
Variant: explicit schedule puzzle
Classic ninja puzzle:
new Ninja("Hattori").eat("rice").sleepFor(3).learn("nunchuck");Same pattern: each call queues an op, the queue executes serially.
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:
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:
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:
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).