finally()
ES2018+Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected).
Syntax
promise.finally(onFinally)Parameters
onFinally Function A function called when the Promise is settled
Return Value
A new Promise
Examples
let isLoading = true;
Promise.resolve('data')
.then(data => console.log(data))
.finally(() => {
isLoading = false;
console.log('로딩 완료');
}); 📌 When to Use
Use finally() for work that must happen no matter how a promise settles: hiding a loading spinner, re-enabling a submit button, closing a database connection or file handle, clearing a timeout, releasing a lock, or decrementing an in-flight request counter. Before finally() existed, that logic had to be duplicated in both then() and catch() — and the two copies inevitably drifted apart. It is the right tool exactly when the cleanup is independent of the outcome: the handler receives no arguments, cannot see the value or the error, and (unless it throws) passes the original settlement through untouched, so inserting it never changes what downstream handlers observe. That transparency also makes finally() safe to add in the middle of a chain, for instance to stop a progress indicator while still letting a later catch() decide how to present the failure. In async/await code the equivalent is the finally block of try/catch/finally, but promise-returning helpers and library code still benefit from the method form because it composes without restructuring. If you find yourself wanting the result inside finally(), that logic belongs in then() or catch() instead — needing the value is the signal you are in the wrong handler.
⚠️ Common Mistakes
Expecting the callback to receive the resolved value or rejection reason. finally() is invoked with no arguments by design — it cannot know how the promise settled. Code like finally(data => save(data)) silently receives undefined and writes nothing useful.
Trying to transform the result by returning a value. A plain return value from finally() is discarded and the original value or error flows through unchanged; only throwing (or returning a promise that rejects) alters the outcome. Transformations belong in then().
Throwing inside finally() and losing the original error. If the cleanup itself fails, its exception replaces whatever rejection was already travelling down the chain, hiding the root cause. Wrap risky cleanup in its own try/catch so a logging failure cannot mask a network failure.
Returning a promise from finally() without realizing it delays settlement. The chain waits for that promise to resolve before continuing — occasionally useful for asynchronous cleanup, but a nasty surprise when the returned promise never settles and the whole chain hangs forever.
Assuming availability everywhere. Promise.prototype.finally shipped in ES2018; very old browsers and Node.js versions below 10 need a polyfill, and TypeScript needs the es2018.promise lib (or later) to know the method exists.
✅ Best Practices
Reserve finally() for outcome-independent side effects — toggling UI state, releasing resources, recording timing metrics. If the logic needs the value or the error, it does not belong there; move it to then() or catch().
Think about ordering relative to catch(): a finally() placed before catch() runs even when the error is later recovered, while one placed after also covers the recovery path itself. Put shared cleanup before the final catch and error-presentation logic after it.
Keep the handler tiny and infallible: set a flag, call clearTimeout, decrement a counter. The less code that can throw inside finally(), the less chance of masking the real failure with a cleanup failure.
Mirror every piece of state you set before starting async work with a finally() that unsets it. Writing loading = true directly above a finally(() => loading = false) makes 'stuck spinner' bugs structurally impossible, because no code path can skip the reset.
⚡ Performance Notes
finally() wraps the promise in one more link, so it adds the same overhead as a then(): one promise allocation plus one microtask hop when the chain settles. Internally it is specified in terms of then() — it calls your handler and then re-delivers the original value or rethrows the original reason, which is also why a promise returned from the handler introduces an extra wait before settlement continues. This cost is trivial, nanoseconds to microseconds, and is dwarfed by whatever asynchronous work the chain actually performs, so choose finally() for correctness and readability rather than avoiding it for speed. The one performance-relevant behavior to remember is that delaying effect: asynchronous cleanup returned from finally() extends the perceived latency of the whole operation as observed by every downstream consumer, so keep cleanup synchronous when response time matters and fire-and-forget any slow, non-critical teardown instead.
🌍 Real World Example
Loading State Manager
A fetch helper that owns its loading indicator. The spinner is shown before the request starts, and a single finally() hides it whether the request resolves, the HTTP status check throws, or the network fails — the success and error handlers at the call site stay focused on their own jobs. Note that the helper still propagates both the value and the error untouched: finally() adds behavior without changing the promise's contract, which is exactly what makes wrappers like this composable across an application.
async function fetchWithLoading(url, loadingElement) {
loadingElement.style.display = 'block';
return fetch(url)
.then(response => {
if (!response.ok) throw new Error('Network error');
return response.json();
})
.finally(() => {
// Always hide loading, success or failure
loadingElement.style.display = 'none';
console.log('Loading complete');
});
}
// Usage
const loader = document.getElementById('loader');
fetchWithLoading('/api/data', loader)
.then(data => console.log('Success:', data))
.catch(err => console.log('Error:', err.message));
// Loading is hidden in both cases