Promise.prototype.finally()

Runs a callback once a promise settles either way, passing the outcome through unchanged.

Signature
promiseInstance.finally(onFinally)
  • Added in JavaScript ES2018

Parameters

ParameterTypeDescription
$onFinallyFunctionA function called asynchronously when the promise settles, whether it fulfilled or rejected. It receives no arguments. If it returns a promise, the resulting promise waits for that to settle; any fulfillment value it produces is ignored.

Return value

PromiseA new Promise, always pending when returned. It settles with the same state and value as the original promise, unless onFinally throws or returns a rejected promise — then it rejects with that reason instead.

Errors

finally() itself does not throw; an error thrown inside onFinally rejects the promise it returns, replacing the original outcome.

finally() is for cleanup that must happen no matter how the operation ended. Its callback takes no arguments — deliberately, because it cannot know whether it is looking at a value or a reason, and it is not supposed to care.

It is a pass-through, not a link

Unlike then() and catch(), whatever onFinally returns is discarded. A fulfilled promise stays fulfilled with its original value; a rejected promise stays rejected with its original reason and still needs a catch further along. This is the whole point: you can drop a finally() into the middle of a chain without disturbing what flows through it.

The two exceptions

The pass-through breaks in exactly two cases. If onFinally throws, or returns a promise that rejects, that error replaces the original outcome — a failing cleanup can mask the real error you were about to report. And if onFinally returns a pending promise, the chain waits for it before continuing, which is how you await an asynchronous teardown.

When to reach for it

Closing handles, clearing timers, decrementing an in-flight counter, turning off a loading flag. If you need the value or the reason, use then() or catch() instead.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Turn off a loading indicator once a request finishes either way
  • Release a lock, connection, or file handle after an operation
  • Clear a timeout that was set alongside the operation
  • Decrement an in-flight request counter for instrumentation

Frequently asked questions

Why does my finally() callback receive no arguments?

By design — finally() is for cleanup that is identical on success and failure, so the specification passes nothing. If you need the value or the error, use then() or catch().

Does finally() handle the rejection for me?

No. It runs your callback and then re-rejects with the same reason, so a chain that ends at .finally() without a .catch() still reports an unhandled rejection.

Can finally() change what the promise resolves to?

Only by failing: throwing inside the callback, or returning a promise that rejects, replaces the outcome with that error. A normal return value is always discarded.

More examples

The value passes through untouched

Both outcomes survive the finally() untouched: the fulfilled promise still yields "original" even though the callback returned something else, and the rejection still carries its original error down to the catch().

Example · javascript
const v = await Promise.resolve("original").finally(() => "ignored return value");
console.log(v);

const r = await Promise.reject(new Error("original error"))
  .finally(() => "ignored")
  .catch((e) => e.message);
console.log(r);

When finally() does change the outcome

A throw inside onFinally overrides the successful result entirely — the fulfillment "good" is lost. The second case shows a returned pending promise delaying the chain by 30ms while still passing the original value through.

Example · javascript
const out = await Promise.resolve("good")
  .finally(() => { throw new Error("cleanup failed"); })
  .catch((e) => "replaced by: " + e.message);
console.log(out);

const waited = await Promise.resolve("value")
  .finally(() => new Promise((res) => setTimeout(res, 30)));
console.log("still:", waited);

Discussion

  • Be the first to comment on this lesson.