When working with asynchronous JavaScript, a common misconception is that a finally
block on an outer promise will wait for all nested promises to complete. This assumption can lead to subtle bugs and unexpected execution order in your code.
The Problem
Consider this code:
promise1().then(() => {
promise2().then(() => {
// something
})
.catch(e => console.error(e));
})
.catch(e => console.error(e))
.finally(() => console.log('all is completed'));
When does the finally
block execute?
Most developers expect it to run after both promise1()
and promise2()
complete. However, it actually executes as soon as promise1()
finishes, regardless of whether promise2()
is still running.
Why This Happens
The key to understanding this behavior lies in how promise chains work:
- Promise chains only wait for returned promises
- When you don’t return a promise from a
.then()
handler, the outer chain doesn’t know about it - The outer promise resolves immediately after the
.then()
callback executes, not after the callback’s internal async operations complete
Execution Order Breakdown
// Step 1: promise1() starts
promise1().then(() => {
// Step 2: promise1() resolved, this callback executes
// Step 3: promise2() starts (but is NOT returned)
promise2().then(() => {
// Step 5: promise2() resolves (happens later)
console.log('promise2 done');
})
.catch(e => console.error(e));
// Step 4: This callback finishes immediately
// The outer promise chain doesn't wait for promise2()
})
.catch(e => console.error(e))
.finally(() => {
// Step 4: Executes right after the .then() callback returns
console.log('all is completed'); // ⚠️ Runs before promise2() finishes!
});
Console output:
all is completed
promise2 done
The Solution: Return Nested Promises
To make the outer promise chain wait for nested promises, you must return them:
promise1().then(() => {
// Return the inner promise
return promise2().then(() => {
console.log('promise2 done');
})
.catch(e => console.error(e));
})
.catch(e => console.error(e))
.finally(() => {
console.log('all is completed'); // ✅ Now runs after both promises
});
Console output:
promise2 done
all is completed
Better Approach: async/await
Modern JavaScript provides a cleaner syntax with async/await, making the execution order more intuitive:
(async () => {
try {
await promise1();
await promise2();
console.log('promise2 done');
} catch (e) {
console.error(e);
} finally {
console.log('all is completed'); // ✅ Guaranteed to run last
}
})();
The finally
block in async/await works identically to the try...catch...finally
statement in synchronous code, making it easier to reason about.
Technical Deep Dive
Promise Resolution Mechanics
When a .then()
handler returns a value:
- Returns a non-promise value: The outer promise resolves immediately with that value
- Returns a promise: The outer promise waits for that promise to settle
- Returns nothing (undefined): The outer promise resolves immediately with
undefined
// Example 1: Returns a promise - outer chain WAITS
promise1().then(() => {
return promise2(); // ✅ Outer chain waits
});
// Example 2: Doesn't return - outer chain DOESN'T WAIT
promise1().then(() => {
promise2(); // ⚠️ Outer chain continues immediately
});
// Example 3: Returns a value - outer chain DOESN'T WAIT for promise2
promise1().then(() => {
promise2();
return 'done'; // ⚠️ Outer chain resolves with 'done', ignores promise2
});
The finally() Guarantee
The finally()
handler has one guarantee: it will execute after the promise settles (resolves or rejects), but it doesn’t care about any non-returned async operations started within the promise chain.
Promise.resolve()
.then(() => {
// Start background operation (not returned)
setTimeout(() => console.log('Background task done'), 1000);
})
.finally(() => {
console.log('Finally block'); // Runs immediately, doesn't wait for setTimeout
});
Common Pitfalls
Pitfall 1: Fire-and-Forget Operations
// ❌ BAD: finally() runs before saveToDatabase completes
fetchData()
.then(data => {
saveToDatabase(data); // Not returned!
})
.finally(() => {
console.log('Done'); // Misleading - database save might not be done
});
// ✅ GOOD: Wait for database save
fetchData()
.then(data => {
return saveToDatabase(data); // Returned!
})
.finally(() => {
console.log('Done'); // Actually done
});
Pitfall 2: Multiple Parallel Operations
// ❌ BAD: finally() doesn't wait for all operations
loadData()
.then(() => {
operation1();
operation2();
operation3();
})
.finally(() => {
console.log('All operations complete'); // Wrong!
});
// ✅ GOOD: Use Promise.all()
loadData()
.then(() => {
return Promise.all([
operation1(),
operation2(),
operation3()
]);
})
.finally(() => {
console.log('All operations complete'); // Correct!
});
Best Practices
- Always return promises from
.then()
handlers when you want the outer chain to wait - Use async/await for better readability and fewer mistakes
- Use Promise.all() for parallel operations
- Use Promise.allSettled() when you want to run
finally
after all promises, even if some reject - Be explicit about fire-and-forget operations with comments
Conclusion
The behavior of finally()
with nested promises is a direct consequence of how promise chains work. Understanding that promise chains only wait for returned promises is crucial for writing correct asynchronous code.
Golden Rule: If you want an outer promise to wait for an inner promise, you must return it.
Further Reading: