JavaScripttry / catch / finally

try / catch / finally

The try statement is JavaScript's structured way to handle exceptions. You put risky code in a try block, intercept failures in a matching catch, and put cleanup that always runs in finally. The shape is small, but the rules around scope, error propagation and return inside finally have a few subtleties worth knowing.

The syntax

A try must be followed by at least one of catch or finally — and may have both. The catch binding is optional since ES2019.

JS
try {
  // code that may throw
} catch (err) {
  // handle the error
} finally {
  // always runs
}

// Optional catch binding (ES2019)
try {
  JSON.parse(input);
} catch {
  return null;
}
What gets caught

A catch block runs when a synchronous throw happens inside the matching try. Asynchronous errors do not propagate to a surrounding try unless you await the promise.

JS
try {
  setTimeout(() => { throw new Error("late"); }, 0);
} catch (err) {
  console.log("never runs — the error is thrown on a different turn");
}

try {
  await Promise.reject(new Error("awaited"));
} catch (err) {
  console.log("caught:", err.message);   // "awaited"
}
Async callbacks need their own catch
Functions you pass into `setTimeout`, `fetch().then` and event listeners run *outside* the surrounding `try`. Each of them needs its own error handling — either an inner `try/catch` or a `.catch` on the returned promise.
Scope of the catch binding

The variable in catch (err) is scoped to the catch block — like a let. It does not leak out, and you cannot redeclare another err in the same block.

JS
function safeJson(s) {
  let result;
  try {
    result = JSON.parse(s);
  } catch (err) {
    console.log("parse failed:", err.message);
    result = null;
  }
  // `err` is out of scope here
  return result;
}
finally runs no matter what

The finally block runs after the try (and any matching catch) finishes, regardless of whether the block returned, threw, or completed normally. It is the right place for cleanup — closing files, releasing locks, restoring UI state.

JS
function withLock(lock, fn) {
  lock.acquire();
  try {
    return fn();
  } finally {
    lock.release();   // runs on normal return, on throw, on early break
  }
}

Even an uncaught error in try runs finally before propagating up the stack.

return and throw inside finally

Two surprising rules:

  • If finally returns a value, it overrides any return from try or catch.

  • If finally throws, the new error replaces any pending error from try or catch.

JS
function weird() {
  try {
    return "from try";
  } finally {
    return "from finally";   // wins — the try return is discarded
  }
}

console.log(weird());        // "from finally"
from finally
Use finally for side effects only
Returning or throwing inside `finally` is almost always a bug. Keep `finally` to cleanup — closing things, restoring state — and let `try`/`catch` decide what the function returns.
Re-throwing and conditional catch

JavaScript has no typed catch (err: HttpError) syntax. Use an instanceof check inside the catch block and re-throw anything you do not handle.

JS
try {
  await loadConfig();
} catch (err) {
  if (err instanceof SyntaxError) {
    console.log("config file is malformed");
    return defaults;
  }
  throw err;          // every other error keeps propagating
}
Nested try blocks

You can nest try statements. The innermost matching catch handles a thrown error; if it re-throws, the outer catch gets a chance.

JS
try {
  try {
    risky();
  } catch (inner) {
    console.log("logging:", inner.message);
    throw inner;             // re-throw so the outer handler can react
  } finally {
    console.log("cleanup");
  }
} catch (outer) {
  console.log("recovered from", outer.message);
}
Errors that escape

An error thrown inside a try and not caught by its catch (because there is no catch, or the catch re-throws) bubbles up the call stack. At the top of an async function this becomes a promise rejection; at the top of a sync stack the engine logs an unhandled error.

JS
function fail() {
  try {
    throw new Error("from inside");
  } finally {
    console.log("finally still runs");
  }
}

try {
  fail();
} catch (err) {
  console.log("caught at the outer level:", err.message);
}
finally still runs
caught at the outer level: from inside
Pattern to remember
Wrap **resource acquisition** with `try / finally` and put the cleanup in `finally`. Wrap **risky operations** with `try / catch` and let `catch` decide whether to log, recover or rethrow. The two patterns are different — do not conflate them.