Promise.prototype.finally()

For work that must happen either way and should not affect the answer. Its callback receives no arguments, precisely because it is not supposed to care whether things went well.

Promise methodES2018Live demo
Common call
p.finally(() => setLoading(false))
Returns
the original outcome, unchanged
Replaces
duplicating cleanup in both then and catch
Watch out
it does NOT handle the rejection — only observes it
promise.finally(onFinallyonFinally — Called with NO arguments when the promise settles, either way. Its return value is ignored — unless it returns a promise, which is awaited before the chain continues.type: Function · required)
→ Promise

Demo

Live evaluation
Try:
Inputs
nnumberthe value to resolve with
boomnumber1 = throw inside finally
Output
await Promise.resolve(42).finally(() => { if (0) throw new Error('from finally'); return 999; })
Pending…

The callback returns 999 in every case, and in the first two that return is simply discarded — the original value comes out unchanged. That is the design: cleanup must not be able to alter the answer. The third case is the one exception: a throw inside finally replaces the outcome with a rejection, which is how a failing cleanup step can mask the error that actually mattered.

Parameters

NameTypeRequiredDescription
onFinallyFunctionyesCalled with NO arguments when the promise settles, either way. Its return value is ignored — unless it returns a promise, which is awaited before the chain continues.

Return value

Promise — A new Promise settling with the ORIGINAL outcome — value or rejection. What the callback returns is discarded; only a throw from it can change the result.

Common patterns

Turn off a loading flag
The canonical use.
setLoading(true);
fetchData().finally(() => setLoading(false));
Release a resource
Runs on success and failure alike.
await useConnection().finally(() => conn.release());
Still handle the error
finally does not catch.
p.finally(cleanup).catch(handle);

Examples

1. The value passes through
await Promise.resolve(42).finally(() => "ignored")
Returns
42
2. Even an explicit return
await Promise.resolve(42).finally(() => 99)
Returns
42
3. A throw DOES replace it
await Promise.resolve(42).finally(() => { throw new Error("from finally"); }).catch(e => e.message)
Returns
'from finally'
4. Rejections pass through too
Promise.reject(new Error("x")).finally(() => {})
Returns
still rejected with x
5. The callback gets no arguments
Promise.resolve(1).finally(v => v)
Returns
v is undefined
6. It is not a handler
Promise.reject(new Error("x")).finally(() => {})
Returns
unhandled rejection unless caught

Pitfalls

1. It does not handle the rejection
A finally on a rejecting promise still leaves that rejection unhandled — the cleanup runs and the error carries on. Code that ends a chain with finally and no catch produces an unhandled rejection while looking like it has error handling.
Still unhandled
Promise.reject(new Error("x")).finally(cleanup)
unhandled rejection
Add a catch
Promise.reject(new Error("x")).finally(cleanup).catch(handle)
handled
2. Its return value is discarded
Deliberate — cleanup should not alter the answer. But it means a finally that computes something useful has computed it for nothing, and a return that looks like it sets the result silently does not.
Ignored
await Promise.resolve(1).finally(() => 99)
1
Use then
await Promise.resolve(1).then(() => 99)
99
3. A throw inside it replaces the outcome
The one way finally changes the result. Cleanup that can fail — releasing a connection that is already closed — will mask the original error with its own, losing the reason the operation failed in the first place.
Masks the original
Promise.reject(new Error("real")).finally(() => { throw new Error("cleanup"); })
rejects with cleanup
Guard the cleanup
p.finally(() => { try { release(); } catch {} })
original error preserved
4. Returning a promise delays the chain
A promise returned from the callback IS awaited, even though its value is ignored. Slow cleanup therefore delays the result, which is occasionally what you want and often an unexplained latency.
Adds the delay
await p.finally(() => slowFlush())
waits for slowFlush
Fire and forget
await p.finally(() => { void slowFlush(); })
returns immediately

When to use

Use it
  • Turning off a loading indicator
  • Releasing connections, locks and file handles
  • Logging that an operation completed, either way
  • Anything currently duplicated in both then and catch
Reach for something else
  • You need to change the result → then or catch
  • You need to handle the error → catch, in addition
  • The cleanup can itself fail → wrap it, or it masks the real error
  • Inside an async function → a try/finally block reads better

Notes

Complexity
O(1) to attach; the callback runs as a microtask
Return
A new Promise mirroring the original settlement
CPython impl
V8: Builtins-promise-finally
Memory
Allocates a new promise
Thread-safe
Single-threaded

FAQ

No. It observes the settlement and passes it on unchanged, so a rejection remains a rejection. You still need a catch — finally is about cleanup, not handling.

p.finally(cleanup).catch(handle);

History

ES2015
Promise shipped without finally; cleanup meant duplicating it in then and catch.
ES2018
finally added, with pass-through semantics deliberately mirroring try/finally.