In a JavaScript async function, does a `finally` block still run when an awaited promise inside the `try` rejects, and what changes if the cleanup code in that `finally` itself needs an `await`?
answer
- cleanup still runs on rejection
- finally is not special in async
- await your asynchronous cleanup too
- throwing in finally replaces the error
basics
~20 sYes — await turns the rejection into a normal throw, so finally runs before the error propagates out. If the cleanup is itself asynchronous you must await it inside the finally, otherwise the function's promise settles while cleanup is still pending and any cleanup failure floats unhandled.
solid answer
~50 s`finally` is not special in async functions. Because `await` rethrows a rejection as an ordinary exception, the usual rule applies: the `finally` block runs on every exit path — normal completion, an early `return`, or an exception in flight — before that outcome propagates out. The async-specific part is the cleanup itself. If you write `finally { conn.close(); }` and `close()` is async, the function's returned promise settles immediately while the close is still pending; a failure in it becomes an orphaned rejection. Writing `finally { await conn.close(); }` suspends the function inside the finally block, so the pending error or return value is held until cleanup finishes, and a cleanup failure is visible. The trap on the other side is that a `throw` inside `finally` replaces the error that was already in flight, so wrap best-effort cleanup in its own try/catch if the original failure is the one you want the caller to see.
code
javascript · 20 linesconst openConn = async () => ({
closed: false,
async close() { this.closed = true; }
});
async function withConn(work) {
const conn = await openConn(); // acquire OUTSIDE the try
try {
const rows = await work(conn);
return rows;
} finally {
await conn.close(); // awaited: settles before we propagate
console.log('closed:', conn.closed);
}
}
withConn(async () => { throw new Error('query failed'); })
.catch(err => console.log('caught:', err.message));
// closed: true
// caught: query failedgo deeper
Know that finally runs whether the try block succeeded, returned, or threw, and that in async code an awaited rejection counts as throwing. Use try/finally to release what you acquired.
Explain why an unawaited cleanup call breaks the guarantee: the function's promise settles while teardown is still pending, and a teardown failure floats. Show the awaited form and where the resource is acquired.
Demonstrate judgment on failure precedence — whether a cleanup error should replace the original — and connect the unawaited-cleanup bug to the production symptom, pool or handle exhaustion under load with orphaned rejections in the logs.
Own the resource-scope convention across services: a withResource-style helper rather than hand-written try/finally at every call site, a policy for best-effort versus must-succeed teardown, and how cleanup latency enters the request's overall budget.
## The rule, restated for async `try { ... } finally { ... }` guarantees the `finally` block runs on every way out of the `try`: falling off the end, `return`, `break`/`continue`, or a thrown exception. In an async function this holds unchanged, because `await` converts a rejection into a thrown exception — an in-flight error is just an ordinary exception, so `finally` sees it like any other. That makes `try/finally` the idiomatic resource scope for async code: acquire before the `try`, release in the `finally`. ```js const conn = await pool.acquire(); try { const rows = await conn.query(sql); return rows; } finally { await conn.release(); } ``` Note there is no `catch` here at all. `try/finally` without a `catch` is a legitimate and common shape: it says "I am not handling this failure, but I am cleaning up before it leaves". ## Why the cleanup needs its own `await` Omitting the `await` on an asynchronous cleanup call breaks the guarantee in a way that is easy to miss, because the code still *looks* correct: ```js } finally { conn.release(); // returns a promise, immediately discarded } ``` With no `await`, the `finally` block completes synchronously. The pending exception (or return value) propagates at once, the async function's promise settles, and the caller proceeds — while the release is still in flight. Two consequences follow. First, ordering guarantees you were relying on are gone: the caller may observe "the operation finished" before the connection is actually back in the pool, which shows up as pool exhaustion under load. Second, if the release rejects, that rejection has no subscriber and becomes an unhandled rejection, disconnected from the request that caused it. With `await conn.release()`, the async function suspends *inside* the finally block. The in-flight completion — value or exception — is preserved across the suspension and resumes propagating only after cleanup settles. That is what makes `await` in `finally` the correct default for asynchronous teardown, at the cost of delaying the caller by however long cleanup takes. ## When the cleanup itself fails A `throw` inside `finally` — including a rejection delivered by an `await` there — **replaces** whatever was propagating. If the query failed with `QueryTimeout` and the release then fails with `PoolClosed`, the caller sees only `PoolClosed`; the diagnostically interesting error is gone. JavaScript has no built-in suppressed-exception mechanism for this, so you decide explicitly: ```js } finally { try { await conn.release(); } catch (cleanupErr) { logger.warn('release failed', cleanupErr); // do not let it win } } ``` Best-effort cleanup gets its own handler; cleanup whose failure genuinely invalidates the result (a commit, a flush that must be durable) is allowed to throw and take over. ## `finally` on the promise versus `finally` in the function `Promise.prototype.finally(fn)` is the chain-level counterpart and behaves consistently with the block form: `fn` receives no argument and its return value is ignored, so the original settlement passes through unchanged — unless `fn` throws or returns a promise that rejects, in which case that failure replaces the original, mirroring the block-level rule. It does wait: if `fn` returns a promise, the derived promise waits for it before passing the original outcome along. Inside an async function, the block form is almost always clearer. ## Interaction with early return `finally` also runs when the `try` block returns, which is what makes it correct for cleanup on the success path too — one block covers both. This is why the resource pattern needs no duplication between the happy path and the failure path, and it is the main argument for `try/finally` over calling `release()` at the end of the body and again in a `catch`. ## What to check in review Three things, in order: is the acquisition **outside** the try (so you never release something you failed to acquire); is every asynchronous call inside the `finally` awaited; and does a cleanup failure deserve to replace the original error, or should it be logged and swallowed. Getting the first wrong produces a `TypeError` on `undefined` that masks the real failure; the second produces phantom pool leaks; the third loses your best diagnostic exactly when you need it.
- Should the resource be acquired inside or outside the try block?Outside. If acquisition itself fails, there is nothing to release, and a `finally` that calls `release()` on an unassigned variable throws a TypeError that masks the original acquisition error. Acquire, then open the try; the finally then only ever runs when the resource genuinely exists.
- What happens to the original error if the awaited cleanup in finally rejects?It is replaced. A throw or rejection from the finally block takes over from whatever was propagating, and JavaScript keeps no suppressed-exception record. If the original failure is the one worth reporting, wrap the cleanup in its own try/catch and log the cleanup failure instead of letting it escape.
- How does Promise.prototype.finally compare with a finally block inside an async function?Same intent, same hazards. The callback takes no argument and its return value is discarded, so the original settlement passes through — but if it returns a promise the chain waits for it, and if that rejects it replaces the original outcome. Inside an async function the block form reads better and is easier to await correctly.
saying these in an interview costs you the question
- Says finally is skipped when the awaited promise rejects
- Leaves an async cleanup call unawaited inside finally
- Assumes a cleanup failure is merged with the original error
- Acquires the resource inside the try block
- Thinks Promise.finally receives the resolved value