Promise Pool (Concurrency Limit)
advancedRun N async tasks with at most K in flight — the production-style p-limit pattern, including ordering and failure trade-offs.
Implement Promise.prototype.finally in JavaScript: transparent passthrough, why the callback gets no arguments, and the async-cleanup delay most candidates miss.
Implement
finally(onFinally)using onlythen: run the callback once the promise settles — either way — while passing the original value or rejection through unchanged.
Small surface, precise semantics. The three rules the interviewer is checking:
onFinally receives no arguments — cleanup must not depend on the outcome.finally is transparent.onFinally throws or returns a rejected promise — then that error replaces the outcome.And one rule almost nobody knows until asked: if onFinally returns a promise, settlement of the chain waits for it.
Promise.prototype.myFinally = function (onFinally) {
return this.then(
(value) =>
Promise.resolve(onFinally()).then(() => value), // forward value
(reason) =>
Promise.resolve(onFinally()).then(() => { throw reason; }) // re-throw reason
);
};Why each piece is load-bearing:
onFinally() with no arguments — rule 1, by construction.Promise.resolve(...) — if onFinally returns a thenable, we wait for it (async cleanup: closing connections, flushing logs). If it returns a plain value, that value is ignored — only the timing matters..then(() => value) — restores the original value after cleanup completes: transparency.{ throw reason; } — re-throws the original rejection so the chain stays rejected. Returning reason instead would convert the rejection into a fulfillment — the classic swallowed-error bug, and the single most planted trap in this question.onFinally propagate naturally through then, giving rule 3 for free.(The spec version also reads this.constructor for subclass support — P.resolve instead of Promise.resolve. Worth a mention, rarely required.)
// 1. Transparent on success
Promise.resolve("data")
.myFinally(() => console.log("cleanup"))
.then(console.log); // cleanup, data
// 2. Transparent on failure — rejection survives the cleanup
Promise.reject(new Error("boom"))
.myFinally(() => console.log("cleanup"))
.catch((e) => console.log(e.message)); // cleanup, boom
// 3. finally's return VALUE is ignored…
Promise.resolve("kept").myFinally(() => "discarded").then(console.log); // kept
// 4. …but its TIMING is not: async cleanup delays the chain
Promise.resolve("later")
.myFinally(() => new Promise((res) => setTimeout(res, 1000)))
.then(console.log); // "later" — after ~1s
// 5. A throwing finally REPLACES the outcome
Promise.resolve("lost")
.myFinally(() => { throw new Error("cleanup failed"); })
.catch((e) => console.log(e.message)); // "cleanup failed"Snippets 3 and 4 together are the discriminating pair: return values ignored, return promises awaited. Most candidates guess one of the two wrong.
The loading-spinner idiom is the canonical one:
setLoading(true);
fetchData()
.then(render)
.catch(showError)
.finally(() => setLoading(false)); // runs on success, failure, AND throw-in-thenWithout finally, setLoading(false) gets duplicated in both handlers — and silently missed when someone adds a new early return. Same shape for: releasing locks, closing modals, ending performance marks, decrementing in-flight counters (which is exactly how the promise pool uses it).
then/catch. finally guarantees the callback can't couple to the result — that's its contract, not a limitation. (An implementation that passes status strings into onFinally isn't an enhancement; it's a different, non-spec method.)finally vs then(fn, fn) — the two-handler version receives the value/reason, must remember to return/rethrow to preserve them, and its return value overwrites the chain's. finally automates the passthrough. Being able to state the difference precisely is the point of the question.finally — it returns a promise; .finally(...).then(...) is normal and common.async/await + try { } finally { } compiles to essentially this method; the semantics were designed to match the sync keyword, including "throw in finally wins."reason instead of re-throwing — converts rejections into successes; downstream catch never fires.Promise.resolve around onFinally() — async cleanup no longer delays settlement (fails snippet 4).onFinally "to be helpful" — breaks the no-arguments contract.Promise.resolve(onFinally()) blocks and then letting them drift during edits — fine as written, but factor if you touch it twice.finally swallows errors like some try/finally misreadings — it doesn't; it propagates them, and a throw inside it wins.Promise.reject(new Error("boom"))
.finally(() => console.log("cleanup"))
.then((v) => console.log("then:", v))
.catch((e) => console.log("catch:", e.message));finally for a promise library that supports subclassing." — const P = this.constructor; P.resolve(...) — the species pattern.then/catch/finally in a chain?" — strictly positional: each link processes in chain order on the microtask queue; finally has no special priority. Output-prediction drills live in the event loop.then(fn, fn) actually better?" — when cleanup does need the outcome (metrics tagging success/failure); then you want the arguments, and you're intentionally not writing finally.then at all, inside your own promise class." — see build a Promise from scratch, where finally is two lines on top of a correct then.Run N async tasks with at most K in flight — the production-style p-limit pattern, including ordering and failure trade-offs.
Implement Promise.race and use it for the timeout pattern every senior interview touches.
Cancel fetch requests, add timeouts with AbortSignal.timeout, and write abortable async utilities.
Implement Promise.any with AggregateError semantics: first success wins, all failures reject.