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.

6 min read

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.

javascriptjavascript
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.

javascriptjavascript
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 try

The 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:

javascriptjavascript
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 finally
finally execution flow

The 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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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 whenPut code after try...catch when
It must run on both success and failureIt should only run on success
It cleans up resources opened in tryIt is the next step after a successful operation
Skipping it would leave broken stateSkipping it is correct when an error occurs
javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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

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.
RunePowered by Rune AI

Frequently Asked Questions

Does finally run even if I return inside try?

Yes. The finally block runs after the return value is computed but before the function actually returns. However, a return inside finally will override the original return value.

Can I use try...finally without a catch block?

Yes. try...finally is valid. If an error is thrown, finally runs first and then the error propagates up. This is useful when you want to clean up resources but let the caller handle the error.

What should I put in finally vs after the try...catch?

Put cleanup that must run whether or not an error occurred in finally. Put code that should only run on success after the entire try...catch block.

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.