InterviewsVector
intermediateCommon5 min read · Updated Jul 18, 2026

Implement Promise.prototype.finally from Scratch

Implement Promise.prototype.finally in JavaScript: transparent passthrough, why the callback gets no arguments, and the async-cleanup delay most candidates miss.


The problem

Implement finally(onFinally) using only then: 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:

  1. onFinally receives no arguments — cleanup must not depend on the outcome.
  2. The original value/reason passes throughfinally is transparent.
  3. unless 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.

Implementation

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.
  • Throws inside 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.)

Verified behavior

// 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.

Where you see it in production

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-then

Without 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).

Edge cases interviewers probe

  • Why no arguments? Deliberate design: cleanup that branches on outcome belongs in 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.
  • Chains continue after finally — it returns a promise; .finally(...).then(...) is normal and common.
  • The try/finally analogyasync/await + try { } finally { } compiles to essentially this method; the semantics were designed to match the sync keyword, including "throw in finally wins."

Common mistakes

  • Returning reason instead of re-throwing — converts rejections into successes; downstream catch never fires.
  • Forgetting Promise.resolve around onFinally() — async cleanup no longer delays settlement (fails snippet 4).
  • Passing the value into onFinally "to be helpful" — breaks the no-arguments contract.
  • Implementing it with two duplicated Promise.resolve(onFinally()) blocks and then letting them drift during edits — fine as written, but factor if you touch it twice.
  • Assuming finally swallows errors like some try/finally misreadings — it doesn't; it propagates them, and a throw inside it wins.

Test yourself

Predict the outputWhat does this code print?
Promise.reject(new Error("boom"))
.finally(() => console.log("cleanup"))
.then((v) => console.log("then:", v))
.catch((e) => console.log("catch:", e.message));

Follow-up questions

  • "Implement finally for a promise library that supports subclassing."const P = this.constructor; P.resolve(...) — the species pattern.
  • "What's the execution order of 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.
  • "When is 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.
  • "Where does the pool use this?" — releasing the concurrency slot regardless of task outcome: promise pool.
  • "Build it without 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.

  • Promise.race

    beginner

    Implement Promise.race and use it for the timeout pattern every senior interview touches.

  • Promise.any

    intermediate

    Implement Promise.any with AggregateError semantics: first success wins, all failures reject.