Finally Block in JavaScript Error Handling Guide
The finally block always runs: after success, after an error, even after a return. Learn how to use it for cleanup, why return in finally is dangerous, and when try...finally is enough.
The finally block is the part of try...catch that always runs, no matter what. If the try block succeeds, finally runs. If try throws an error, finally runs.
Even if try has a return statement, finally runs before the function actually returns.
function readAndProcess(path) {
const handle = openFile(path);
try {
return parseData(handle);
} finally {
closeFile(handle);
}
}This is the core use of finally: resource cleanup that must happen no matter what. The file handle closes whether parseData succeeds, fails, or the function returns early.
When finally runs
finally executes after the try block, and the catch block if one is present, but before control returns to the caller.
function showWhenFinallyRuns() {
try {
console.log("1. Inside try");
return "result from try";
} finally {
console.log("2. Inside finally");
}
}
console.log(showWhenFinallyRuns());
// 1. Inside try
// 2. Inside finally
// result from tryThe return in try computes its value immediately, but the function does not exit until finally completes. With an error present, catch runs before finally, in the same order it would without a return:
function showFinallyWithError() {
try {
throw new Error("oops");
} catch (e) {
console.log("Inside catch:", e.message);
} finally {
console.log("Inside finally");
}
}
showFinallyWithError();
// Inside catch: oops
// Inside finallyThe finally block always executes. The only question the diagram highlights is whether it changes the outcome by using its own return or throw.
The return override trap
A return, throw, break, or continue inside finally overrides whatever the try or catch block intended to do:
function dangerous() {
try {
return "from try";
} finally {
return "from finally";
}
}
console.log(dangerous());This logs "from finally", not "from try", because the finally block's own return statement wins. The same thing happens with throw, and it is even more dangerous there, since it silently discards the original error:
function dangerousThrow() {
try {
throw new Error("original error");
} finally {
throw new Error("finally error");
}
}
try {
dangerousThrow();
} catch (e) {
console.log(e.message); // "finally error", the original is lost forever
}The rule is simple: never put return, throw, break, or continue inside finally. The block should only contain cleanup code, such as closing connections, clearing state, or removing listeners. If you need conditional logic based on success or failure, put it in catch or after the block instead.
try...finally without catch
You do not need a catch block. try...finally is a valid form on its own:
function withCleanup(fn) {
const start = performance.now();
try {
return fn();
} finally {
console.log(`Took ${performance.now() - start}ms`);
}
}If fn throws, the timing still logs, and then the error propagates up to the caller unchanged. This pattern is perfect when you want to observe or clean up but let the caller handle the actual error. A practical use is temporarily changing a global setting and restoring it afterward:
function runInDebugMode(fn) {
const previous = appConfig.debug;
appConfig.debug = true;
try {
return fn();
} finally {
appConfig.debug = previous;
}
}Without finally, a thrown error inside fn would leave debug permanently set to true, since the line that restores it would never run.
Real-world cleanup patterns
Closing a database connection is one of the most common uses of finally, since the pool needs the client back whether the query succeeded or not:
async function queryDatabase(sql) {
const client = await pool.connect();
try {
return await client.query(sql);
} finally {
client.release();
}
}Clearing a loading state follows the same shape as the database example above, so the spinner cannot get stuck visible on screen after a failed request:
async function loadDashboard() {
showSpinner();
try {
const data = await fetchDashboardData();
render(data);
} catch (error) {
showErrorBanner("Failed to load dashboard");
} finally {
hideSpinner();
}
}If hideSpinner were placed after the try...catch instead of inside finally, an unhandled error in catch would leave the spinner visible forever, since nothing after a rethrow would ever run.
finally vs code after try...catch
| Put code in finally when | Put code after try...catch when |
|---|---|
| It must run on both success and failure | It should only run on success |
| It cleans up resources opened in try | It is the next step after a successful operation |
| Skipping it would leave broken state | Skipping it is correct when an error occurs |
try {
const file = openFile();
process(file);
} finally {
closeFile(file); // Must run either way
}Code placed after the whole try...catch block, by contrast, only executes when nothing throws, which is exactly why sendSuccessNotification below waits until after closeFile instead of living in finally:
try {
const file = openFile();
process(file);
closeFile(file);
sendSuccessNotification(); // Only reached if no error
} catch (e) {
console.error(e);
}Common mistakes
Accidentally overriding a return value is the most common mistake. If the try block returns a value but finally also returns something, or changes the value being returned, the actual result can surprise you. Avoid return in finally completely.
Putting non-cleanup logic in finally is a quieter mistake. If the code is not about releasing resources or restoring state, it probably belongs outside the block:
try {
doWork();
} finally {
trackEvent("work_completed"); // This is business logic, not cleanup
}Analytics tracking depends on whether the work actually succeeded, so it reads more clearly split across catch and the success path instead of living unconditionally in finally:
try {
doWork();
trackEvent("work_completed");
} catch (e) {
trackEvent("work_failed");
}Assuming finally catches everything is the last mistake. If the try block calls process.exit in Node.js, or the browser tab closes, finally will not run. It guards against JavaScript exceptions and return statements, not process termination.
For the full try...catch syntax including conditional catch and nesting, see the try...catch guide. For more on the throw statement, see throwing errors in JavaScript.
Rune AI
Key Insights
- The finally block always executes, regardless of whether an error was thrown or caught.
- Use finally for resource cleanup: closing files, clearing timers, removing listeners.
- A return in finally overrides any return or throw from try or catch, which is rarely intended.
- try...finally without catch is valid and useful when you want cleanup without handling the error.
- Code after the try...catch block runs only on success; code in finally runs on success and failure.
Frequently Asked Questions
Does finally run even if I return inside try?
Can I use try...finally without a catch block?
What should I put in finally vs after the try...catch?
Conclusion
finally is the cleanup guarantee. It runs no matter how the try block exits: normally, via return, via throw, or via break or continue. Use it to close files, remove event listeners, clear timers, and reset state. But never put control flow statements in finally, since a return or throw inside it overrides the original outcome, and that is almost never what you want.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.