JS Try Catch Tutorial Advanced Error Handling

try...catch catches runtime errors so your code can recover instead of crashing. Learn the full syntax, finally cleanup, nested handling, conditional catch, and rethrowing patterns.

8 min read

A try...catch statement lets you run code that might fail and recover from the failure instead of letting it crash your script. When an error is thrown inside the try block, execution jumps straight to the catch block, where you can log it, show a message, or attempt a fix.

Without this statement, any unhandled runtime error stops the rest of your code from running. With it, you decide what happens next.

javascriptjavascript
try {
  const data = JSON.parse("not valid json");
  console.log("This never runs");
} catch (error) {
  console.log("Parsing failed:", error.message);
  // Parsing failed: Unexpected token 'o', "not valid json" is not valid JSON
}
 
console.log("This still runs after the catch block");

JSON.parse throws a syntax error when given invalid input. The catch block catches that error, logs a message, and the code after the statement continues normally as shown by the last line and its comment above. Without the wrapper, that error would have terminated the script before that last line ever ran.

Try, catch, and finally syntax

A try block can pair with catch, finally, or both. Pick the shape based on whether you need to react to the error, clean up afterward, or both.

javascriptjavascript
try {
  riskyStep();
} catch (error) {
  console.log("Handled:", error.message);
} finally {
  console.log("Always runs, error or not");
}

Add finally when you always need cleanup to happen, such as closing a file or clearing a loading spinner. Drop catch entirely and use try with only finally when you want cleanup to run but you want the error to keep propagating up to the caller.

If you do not need the error object itself, the catch binding is optional in modern JavaScript:

javascriptjavascript
function isValidJSON(text) {
  try {
    JSON.parse(text);
    return true;
  } catch {
    return false;
  }
}

This shorter form is common for boolean checks like isValidJSON, where the only thing that matters is whether an error happened, not what it says.

The error object

Inside catch, the caught value is usually an Error instance with three useful properties: name, message, and stack. The stack property is the most useful for debugging, since it shows the exact line and call chain where the error originated.

PropertyDescription
nameThe error type, such as TypeError, ReferenceError, SyntaxError, or Error
messageA human-readable description of what went wrong
stackA string trace of function calls leading to the error (non-standard but available everywhere)
javascriptjavascript
try {
  undefinedVariable.property;
} catch (error) {
  console.log(error.name);    // "ReferenceError"
  console.log(error.message); // "undefinedVariable is not defined"
}

Reading undefinedVariable before it exists anywhere in scope throws a reference error. Checking error.name first is how the conditional catch pattern later in this article decides which branch to run.

The finally block

finally runs no matter what happens in try or catch. Even if the try block throws, returns, or breaks, finally executes before control leaves the construct.

javascriptjavascript
function readConfig(path) {
  const file = openFile(path);
  try {
    return parseConfig(file);
  } catch (error) {
    return { defaults: true };
  } finally {
    closeFile(file);
  }
}

This is the correct pattern for resource cleanup: open the resource, use it inside try, close it inside finally. The file closes whether parseConfig succeeds, fails, or the function returns early.

One important caveat: a return or throw inside finally overrides any return or throw from try or catch.

javascriptjavascript
function demo() {
  try {
    return "from try";
  } finally {
    return "from finally";
  }
}
 
console.log(demo());

This logs "from finally", not "from try", because the return inside finally wins. Avoid control flow statements in finally. Keep it for cleanup only: closing files, removing event listeners, clearing timers, or resetting state.

Conditional catch with instanceof

A single catch block handles every type of error by default. Use instanceof to respond differently depending on which error type was thrown:

javascriptjavascript
try {
  processUserInput(input);
} catch (error) {
  if (error instanceof TypeError) {
    showMessage("Please check your input format.");
  } else if (error instanceof RangeError) {
    showMessage("The value is outside the allowed range.");
  } else {
    console.error(error);
    showMessage("Something unexpected happened.");
  }
}

This pattern is called conditional catch. You handle specific errors you expect and let unexpected ones surface to a higher-level handler or a crash reporter, instead of treating every failure the same way.

Rethrowing errors

Sometimes you want to catch an error, do something with it, and then let it continue up the call stack as if it was never caught. Log the details, then throw the same error object again:

javascriptjavascript
try {
  saveToAPI(data);
} catch (error) {
  console.error("API save failed, details:", error);
  throw error;
}

Rethrowing is also how you filter errors inside a conditional catch. If an error is not one you know how to handle, send it back up instead of swallowing it:

javascriptjavascript
try {
  performOperation();
} catch (error) {
  if (error instanceof ValidationError) {
    handleValidation(error);
  } else {
    throw error;
  }
}

This prevents silently hiding errors that should be handled by a caller further up the chain, which is one of the most common sources of hard-to-debug production bugs.

Nested try...catch

You can nest one try...catch inside another. An error thrown in the inner block is caught by the nearest enclosing catch. If the inner catch rethrows, the outer catch receives it next:

javascriptjavascript
try {
  try {
    throw new Error("inner problem");
  } catch (error) {
    console.log("Inner catch:", error.message); // "Inner catch: inner problem"
    throw error;
  } finally {
    console.log("Inner finally"); // runs before the outer catch sees the error
  }
} catch (error) {
  console.log("Outer catch:", error.message); // "Outer catch: inner problem"
}

The three log lines fire in order: the inner catch first, then the inner finally, then the outer catch, since the rethrow only reaches the outer block after the inner finally has finished running.

Nested try...catch error propagation

The outer catch only fires if the error escapes the inner catch, either because the inner block has no catch or because the inner catch rethrows, as shown by the two paths in the diagram above.

try...catch with async code

try...catch only catches synchronous errors. Errors thrown inside a setTimeout or setInterval callback are not caught unless the try...catch wraps the callback itself, not the call that schedules it:

javascriptjavascript
setTimeout(() => {
  try {
    throw new Error("Timer error");
  } catch (error) {
    console.log("Caught:", error.message);
  }
}, 1000);

Wrapping the outer setTimeout call in try...catch would not work, because the callback runs later on its own turn of the event loop, after the surrounding try block has already finished. For promises and async/await, try...catch works directly with await:

javascriptjavascript
async function fetchUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error("Failed to fetch user:", error.message);
    return null;
  }
}

When you await a promise, a rejection becomes a synchronous throw that try...catch can handle. Without await, you need .catch() on the promise chain instead. For more on async patterns, see the JavaScript Promises guide and async/await tutorial.

Common mistakes

Catching too broadly is the most common mistake. A catch block that silently swallows every error hides real bugs instead of surfacing them:

javascriptjavascript
try {
  doWork();
} catch (e) {
  if (e instanceof ExpectedError) {
    handleExpected(e);
  } else {
    console.error(e);
  }
}

An empty catch block with no branching, like catch (e) {}, throws away information you will need later when something breaks in production. Always log or rethrow errors you did not expect.

Putting too much code inside the try block is the second common mistake. The try block should contain only the code that might throw. Non-throwing setup code, like sending a notification after everything else succeeded, belongs outside it:

javascriptjavascript
function process() {
  let config, result;
  try {
    config = loadConfig();
    result = compute(config);
    save(result);
  } catch (e) {
    console.error(e);
    return;
  }
  sendNotification();
}

Keeping sendNotification outside the try block makes it clear that notification failures are a separate concern from the operations that can actually fail during processing.

Forgetting that event handlers need their own try...catch is the third mistake. Each event listener or timer callback runs in its own execution context, so the wrapper has to go inside the handler body, not around the call that registers it:

javascriptjavascript
button.addEventListener("click", () => {
  try {
    riskyOperation();
  } catch (e) {
    console.error("Click handler failed:", e);
  }
});

Wrapping addEventListener itself in try...catch would never catch anything, because addEventListener only registers the handler. It does not run riskyOperation until the user actually clicks the button, well after the surrounding try block has exited.

Rune AI

Rune AI

Key Insights

  • try...catch catches runtime errors and lets your code recover instead of crashing.
  • The finally block always runs, making it the right place for resource cleanup.
  • Use instanceof to conditionally handle specific error types and rethrow unexpected ones.
  • try...catch only catches synchronous errors inside the try block, not errors in timers or promises unless you wrap the callback.
  • Avoid return, throw, break, or continue inside finally, they override the original control flow.
RunePowered by Rune AI

Frequently Asked Questions

Does try...catch catch all JavaScript errors?

try...catch only catches errors thrown in synchronous code inside the try block. It does not catch syntax errors in the same script (the parser finds those first), and it does not catch errors in scheduled callbacks like setTimeout unless the try...catch wraps the callback itself.

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

Yes. You can use try...finally without catch. The finally block will still run, and any error thrown in try will propagate up after finally completes.

What happens if I return inside a finally block?

A return, throw, break, or continue inside finally overrides any return or throw from the try or catch block. This is rarely intended, so avoid control flow statements in finally.

Conclusion

try...catch is the backbone of resilient JavaScript. Use it to handle expected failures gracefully, clean up resources with finally, and decide which errors to handle versus rethrow. Combined with the Error constructor and custom error types, it gives you full control over how your program responds when things go wrong.