In JavaScript, when does a `finally` block actually run? Does it still execute if the `try` block returns a value, or if an error is thrown that no `catch` clause handles?
answer
- cleanup slot, not error slot
- every exit path, not only throws
- return value computed, then finally
- runs while an exception unwinds
- only a dead job skips it
basics
~20 sA JavaScript finally block runs on every exit from its try statement - normal completion, an early return, a break or continue, a caught error, and an error still propagating outward - which is exactly why cleanup belongs there.
solid answer
~40 s`finally` is a guarantee about leaving the `try` statement, not about errors. It runs after the try block finishes normally, after a `catch` clause handles an error, when the try or catch block executes a `return`, `break` or `continue`, and while an unhandled exception is unwinding past it on the way to some outer handler. In the `return` case the order matters: the return expression is evaluated first, its value is set aside, `finally` runs, and only then does control actually leave the function. With nested statements the inner `finally` runs before the outer `catch` sees the error. The only things that skip it are things that never unwind the stack at all - an infinite loop, a hard process exit, or the page going away.
code
javascript · 18 linesfunction nested() {
try {
try {
throw new Error('boom');
} finally {
console.log('inner finally');
}
} catch (err) {
console.log('outer catch:', err.message);
} finally {
console.log('outer finally');
}
}
nested();
// inner finally
// outer catch: boom
// outer finallygo deeper
Be able to say plainly that finally runs on every way out of the try statement - success, return, break, caught error, and an error still travelling outward - and that it is the natural home for cleanup.
Explain the ordering mechanically: the return expression is evaluated, the value is parked, finally runs, then control leaves. Be ready to trace a nested example and show the inner finally firing before the outer catch.
Show judgment about what you are willing to trust finally with. In-process state such as locks, counters and spinners, yes; anything that must survive a process kill needs an out-of-process guarantee instead.
Frame it as a contract boundary: finally gives deterministic in-process cleanup and nothing more, so durability, idempotency and reconciliation have to live elsewhere in the design rather than being smuggled into a cleanup block.
## The shape of the statement A `try` statement is a block followed by a `catch` clause, a `finally` clause, or both. `try { }` on its own is a SyntaxError - the parser requires at least one of the two clauses. `catch` is the *handling* clause: it runs only when something was thrown. `finally` is the *exit* clause: it runs no matter how control leaves the try statement. That distinction is the whole question. Many people file `finally` mentally under "error handling" and are then surprised that it fires on a perfectly successful path. ## Every exit path Concretely, `finally` runs when: - the try block reaches its closing brace normally; - the try block throws and a `catch` clause on the same statement handles it (the catch body runs first, then `finally`); - the `catch` body itself throws - `finally` still runs, and the new error keeps propagating; - the try or catch block executes `return`, so the function is about to exit; - the try or catch block executes `break` or `continue` to jump out of an enclosing loop or labelled block; - the try block throws and *nothing* on this statement catches it - `finally` runs as the exception unwinds past this frame toward an outer handler or the top level. That last case is the one juniors most often get wrong. An uncaught throw does not teleport out of the function; the engine unwinds the stack frame by frame, and every `finally` it passes through runs on the way out. ## The return case in slow motion ```js function demo() { try { console.log('1 try'); return compute(); } finally { console.log('2 finally'); } } ``` The engine evaluates `compute()`, stores the resulting value as a pending completion, runs the `finally` block, and only then hands the stored value back to the caller. So the log order is `1 try`, then `2 finally`, and the caller receives whatever `compute()` produced. `finally` is not "after the function returns" - it is *inside* the return, between computing the value and delivering it. ## Nested statements unwind inside out ```js try { try { throw new Error('boom'); } finally { console.log('inner finally'); } } catch (err) { console.log('outer catch:', err.message); } finally { console.log('outer finally'); } // inner finally // outer catch: boom // outer finally ``` The error leaves the inner block, the inner `finally` runs while it is still in flight, then the outer `catch` receives it, and the outer `finally` closes the statement. Cleanup at each level happens before any handler further out sees the failure - which is what you want, since the inner level is the one that knows what it opened. ## Inside loops A `try` statement inside a loop body is a fresh statement on every iteration, so its `finally` runs once per iteration - including on the iteration where you `continue` or `break`: ```js for (const item of items) { try { if (!item.ready) continue; process(item); } finally { releaseSlot(item); } } ``` `releaseSlot` runs for every item, skipped ones included. ## When finally does *not* run There is no unwinding, so no `finally`, when the job simply stops: an infinite loop or a blocking call that never returns; a hard exit of the host process; the browser tab or worker being torn down; a crash of the engine itself. In other words `finally` is a guarantee about JavaScript control flow, not a guarantee about the machine. Treat it as reliable for releasing in-process things (locks, counters, spinners, timers) and not as a substitute for external cleanup that must survive a crash. ## What belongs in it Put in `finally` the actions that must happen whether or not the work succeeded: restoring a flag you set, decrementing an in-flight counter, hiding a loading indicator, releasing a lock, closing a handle. Do not put the success path there - it runs on failure too. And keep the block boring: it executes while an error may already be travelling, and anything abrupt it does can change the outcome of the whole statement.
- If the try block runs `return computeValue()` and the finally block logs, which happens first?`computeValue()` is evaluated first and its result is held as a pending return value; then the `finally` block runs and logs; then the function actually returns that stored value. So the log appears before the caller sees anything, even though the `return` statement was written first.
- Are there situations where a finally block genuinely never runs?Yes - any case where the stack is never unwound. An infinite loop or a call that never returns keeps control inside the try block forever. A hard exit of the host process, a tab or worker being destroyed, or an engine crash all stop execution without unwinding. `finally` guarantees JavaScript control flow, not machine survival.
- Does a finally block run once or once per iteration when the try statement sits inside a loop?Once per iteration. Each pass through the loop body executes the `try` statement afresh, so its `finally` runs every time - including on iterations that `continue` or that `break` out of the loop, since those are exits from the try statement too.
Think of it as the light switch by the door: it flips whether you leave the room calmly, in a hurry, or chased out by a fire.
saying these in an interview costs you the question
- Thinks finally only runs when an error was thrown
- Says an uncaught error skips finally and exits immediately
- Believes finally runs after the caller receives the return value
- Assumes a try block with no catch is a syntax error
- Claims finally always runs, even on a hard process exit