A program that opens something has to close it again: a loading indicator, a lock, a connection. The closing line always comes after the work, and an error raised during that work carries it away.
finally answers that precise problem, and no other: it reserves a zone that nothing can skip.
Definition
finally ends a try structure and runs whatever happens: the work succeeded, an error was caught by a catch, an error was caught by nobody, or the function left early through a return. No other block offers that guarantee.
let spinnerVisible = false;
function load(read) {
spinnerVisible = true;
try {
return read();
} finally {
spinnerVisible = false;
}
}
try {
load(() => { throw new Error("network unavailable"); });
} catch (error) {
console.log(error.message, "| spinner still visible:", spinnerVisible);
}
// network unavailable | spinner still visible: falseThe indicator is removed even though the read failed, which a line placed after the structure would not have guaranteed.
It runs even after a return
This is the behavior that surprises people most, and the one that makes the block genuinely useful. It is tempting to picture a return ending the function on the spot. That is not what happens.
function value() {
try {
return "came from the try";
} finally {
console.log("the finally runs first");
}
}
console.log(value());
// the finally runs first
// came from the tryThe engine evaluates the expression after the return, sets the result aside, runs the finally, then hands the value back. The same mechanism applies to break and continue inside a loop.
Never write a return inside the finally. It overwrites the value prepared by the try and, far worse, it makes a live error disappear: the function quietly returns a wrong result. A throw written there causes the same erasure.
The exact order of execution
When a structure brings all three blocks together, saying from memory which one runs first quickly becomes guesswork. Each row of the table reads as a timeline.
| Situation | What runs, in order |
|---|---|
| No error | try, then finally |
| Caught error | try up to the error, then catch, then finally |
| Uncaught error | try up to the error, then finally, then the error leaves |
| Exit through return | try up to the return, then finally, then the value leaves |
The third row is the one that matters most in production: cleanup happens before the caller receives anything at all. One point often forgotten: an error raised inside the catch also triggers the finally before leaving.
Frequently asked questions
Can a finally be written without a catch?
Yes, the form is valid and in fact very common. It says something precise: this function makes no claim to handle the error, it lets it travel up to the code that will, but it insists on tidying up before it goes.
Does the block run when the error is caught nowhere?
Yes, in full, and then the error carries on up to the console or to the end of the process. The only situations that prevent it fall outside the language: a closed tab, a process killed by the operating system, a power cut.
How is it different from a line written after the structure?
A line placed after the block runs only if execution reaches it. It is therefore skipped by a return, by an uncaught error, and by an error raised from the catch. The finally covers all three paths, which makes it the only safe place for cleanup that must happen.