How to Handle Errors with try...catch in JavaScript
No matter how carefully you write code, errors are inevitable. A network request fails, a user provides unexpected input, a JSON string is malformed, or a property is accessed on undefined. Without proper error handling, a single unexpected error crashes your entire script and leaves users staring at a broken page.
JavaScript provides the try...catch statement as the primary mechanism for catching and handling runtime errors gracefully. Instead of letting your program crash, you can intercept errors, respond to them appropriately (log them, show a user-friendly message, try an alternative approach), and keep your application running.
This guide covers every aspect of error handling in JavaScript, from the fundamental distinction between syntax and runtime errors, through the try...catch...finally statement, to advanced patterns like rethrowing and global error handlers.
Syntax Errors vs. Runtime Errors
Before diving into try...catch, you need to understand the two fundamentally different categories of errors in JavaScript. They occur at different stages and are handled differently.
Syntax Errors (Parse-Time Errors)
Syntax errors happen when the JavaScript engine parses your code, before any of it runs. The engine reads your source code, tries to understand its structure, and fails because the code is not valid JavaScript.
// Missing closing brace: syntax error
function greet( {
console.log("Hello");
// Unexpected token: syntax error
let x = 5 +* 3;
// Invalid assignment: syntax error
const 123abc = "test";
When the engine encounters a syntax error, it cannot execute any code in that script. The entire script block is rejected. You will see an error in the browser console before any of your code runs.
Runtime Errors (Exceptions)
Runtime errors happen during execution, after the code has been successfully parsed. The syntax is valid JavaScript, but something goes wrong when the code actually runs.
// This parses fine: valid syntax
// But throws a ReferenceError at runtime because 'xyz' doesn't exist
console.log(xyz);
// Valid syntax, but TypeError at runtime
const obj = null;
obj.property;
// Valid syntax, but RangeError at runtime
const arr = new Array(-1);
Why This Distinction Matters
try...catch can only handle runtime errors. It cannot catch syntax errors because the code never gets to the execution stage where try...catch could intercept anything.
// This CANNOT catch the syntax error: the whole script fails to parse
try {
let x = 5 +* 3; // SyntaxError during parsing
} catch (e) {
console.log("Caught:", e.message); // Never runs
}
The script above never executes at all. The engine sees the syntax error during parsing and rejects the entire script.
// This CAN catch runtime errors: the code parses fine
try {
let result = someUndefinedVariable + 5; // ReferenceError at runtime
} catch (e) {
console.log("Caught:", e.message); // "someUndefinedVariable is not defined"
}
There is one subtle exception. If you load a script dynamically (for example, through eval() or new Function()), the parsing of that dynamic code happens at runtime. In that case, a SyntaxError from the dynamically parsed code becomes a runtime error that try...catch can intercept:
try {
eval("let x = 5 +* 3"); // Parsed at runtime → catchable SyntaxError
} catch (e) {
console.log("Caught:", e.name); // "SyntaxError"
console.log("Caught:", e.message); // "Unexpected token '*'"
}
The try...catch Statement
The try...catch statement is the core mechanism for error handling. It lets you "try" a block of code and "catch" any errors that occur during execution.
Basic Syntax
try {
// Code that might throw an error
const data = JSON.parse('{"name": "Alice"}');
console.log(data.name); // "Alice"
} catch (error) {
// Code that runs if an error occurred in the try block
console.log("Something went wrong:", error.message);
}
How It Works Step by Step
When no error occurs:
try {
console.log("Step 1: Starting"); // Runs
const x = 10 + 5; // Runs
console.log("Step 2: x =", x); // Runs ("Step 2: x = 15")
console.log("Step 3: Finished"); // Runs
} catch (error) {
console.log("Error caught!"); // SKIPPED (no error occurred)
}
console.log("Step 4: After try/catch"); // Runs
Output:
Step 1: Starting
Step 2: x = 15
Step 3: Finished
Step 4: After try/catch
The catch block is never entered. Execution flows from the try block directly to the code after try...catch.
When an error occurs:
try {
console.log("Step 1: Starting"); // Runs
const data = JSON.parse("{bad}"); // ERROR (throws SyntaxError)
console.log("Step 2: Parsed"); // SKIPPED (error already thrown)
console.log("Step 3: Finished"); // SKIPPED
} catch (error) {
console.log("Error caught!"); // Runs
console.log("Message:", error.message); // Runs
}
console.log("Step 4: After try/catch"); // Runs (execution continues)
Output:
Step 1: Starting
Error caught!
Message: Unexpected token b in JSON at position 1
Step 4: After try/catch
The moment JSON.parse throws an error, execution jumps immediately to the catch block. All remaining statements in the try block are skipped. After the catch block finishes, execution continues normally with the code after try...catch.
The critical benefit: the program does not crash. Without try...catch, the SyntaxError from JSON.parse would terminate the script entirely.
try...catch Works Only for Runtime Errors
As discussed above, try...catch operates on code that is already parsed and running. It intercepts errors that happen during execution (runtime errors, also called exceptions), not errors that happen during parsing.
// Parse error (try...catch cannot help)
// This entire script would fail to load:
// try {
// function( {{{ // Broken syntax
// } catch (e) {
// // Never reached
// }
// Runtime error (try...catch works perfectly)
try {
undeclaredVariable; // ReferenceError at runtime
} catch (e) {
console.log("Caught:", e.message); // "undeclaredVariable is not defined"
}
Also, try...catch only catches errors that occur within the try block's synchronous execution. Errors in code scheduled for later (callbacks, timers) are not caught.
try...catch Works Synchronously (Not for Async Callbacks)
This is one of the most common sources of confusion. try...catch wraps around the synchronous execution of the try block. If code inside the try block schedules a callback for later execution, errors in that callback happen after the try...catch has already finished.
The Problem: setTimeout
try {
setTimeout(function() {
undeclaredVariable; // ReferenceError (but when does it throw?)
}, 1000);
console.log("Timer scheduled"); // Runs immediately
} catch (e) {
console.log("Caught:", e.message); // Never runs for the timer error!
}
What happens:
- The
tryblock runs synchronously setTimeoutis called, which schedules the callback for later. It does not run the callback now.console.log("Timer scheduled")runs- The
tryblock finishes successfully (no error occurred during synchronous execution) - One second later, the callback runs outside the
try...catchscope - The
ReferenceErroris thrown with notry...catchto catch it - The error goes to the global error handler (or crashes)
The Fix: Put try...catch Inside the Callback
setTimeout(function() {
try {
undeclaredVariable; // ReferenceError
} catch (e) {
console.log("Caught inside callback:", e.message);
// "undeclaredVariable is not defined"
}
}, 1000);
Now the try...catch is inside the callback, so it is active when the error occurs.
The Same Problem with Event Listeners
// WRONG: try...catch is gone by the time the click happens
try {
document.getElementById("btn").addEventListener("click", function() {
undeclaredVariable; // Not caught by the outer try...catch
});
} catch (e) {
console.log("Caught:", e.message);
}
// CORRECT: try...catch inside the handler
document.getElementById("btn").addEventListener("click", function() {
try {
undeclaredVariable;
} catch (e) {
console.log("Caught:", e.message);
}
});
try...catch catches errors that occur during the synchronous execution of the try block. Any asynchronous callbacks (timers, event handlers, XHR/fetch callbacks) need their own try...catch inside the callback. For Promises and async/await, there are dedicated error handling patterns covered in the Promises module.
The Error Object: name, message, stack
When an error is thrown and caught, the catch block receives an error object that contains information about what went wrong. All built-in error objects have three standard properties.
name
The type of the error as a string. For built-in errors, this matches the constructor name:
try {
null.property;
} catch (e) {
console.log(e.name); // "TypeError"
}
try {
undeclared;
} catch (e) {
console.log(e.name); // "ReferenceError"
}
try {
JSON.parse("{bad}");
} catch (e) {
console.log(e.name); // "SyntaxError"
}
message
A human-readable description of the error:
try {
const obj = undefined;
obj.method();
} catch (e) {
console.log(e.message);
// "Cannot read properties of undefined (reading 'method')"
}
try {
decodeURIComponent("%");
} catch (e) {
console.log(e.message);
// "URI malformed"
}
stack
A string containing the stack trace: the chain of function calls that led to the error. This is the most valuable property for debugging. While not part of the official ECMAScript standard, it is supported by all major environments.
function innerFunction() {
throw new Error("Something broke");
}
function middleFunction() {
innerFunction();
}
function outerFunction() {
middleFunction();
}
try {
outerFunction();
} catch (e) {
console.log(e.stack);
}
Output (Chrome/Node.js):
Error: Something broke
at innerFunction (<anonymous>:2:9)
at middleFunction (<anonymous>:6:3)
at outerFunction (<anonymous>:10:3)
at <anonymous>:14:3
The stack trace reads top-to-bottom: the error originated in innerFunction, which was called by middleFunction, which was called by outerFunction.
Using All Three Properties Together
function processUserData(data) {
try {
const user = JSON.parse(data);
console.log(user.name.toUpperCase());
} catch (e) {
console.error(`[${e.name}] ${e.message}`);
console.error("Stack trace:", e.stack);
// In production, you'd send this to a logging service
// logErrorToService({ name: e.name, message: e.message, stack: e.stack });
}
}
processUserData('{"name": null}');
// [TypeError] Cannot read properties of null (reading 'toUpperCase')
// Stack trace: TypeError: Cannot read properties of null...
Other Properties
Some environments and error types provide additional properties:
try {
eval("function(");
} catch (e) {
console.log(e.name); // "SyntaxError"
console.log(e.message); // "Unexpected end of input"
console.log(e.lineNumber); // Line number (Firefox only)
console.log(e.columnNumber); // Column number (Firefox only)
console.log(e.fileName); // File name (Firefox only)
}
The stack, lineNumber, columnNumber, and fileName properties are non-standard but widely supported. Do not rely on their exact format for programmatic parsing, as it differs between engines. Use them for logging and debugging only.
Built-In Error Types
JavaScript has seven built-in error constructors. Each represents a specific category of error and inherits from the base Error constructor.
Error
The base error type. Used for generic errors and as the base class for custom errors:
const err = new Error("Something went wrong");
console.log(err.name); // "Error"
console.log(err.message); // "Something went wrong"
SyntaxError
Thrown when code cannot be parsed or when JSON.parse receives invalid JSON:
try {
JSON.parse("not valid json");
} catch (e) {
console.log(e.name); // "SyntaxError"
console.log(e.message); // "Unexpected token 'o', "not valid json" is not valid JSON"
}
try {
eval("if(");
} catch (e) {
console.log(e.name); // "SyntaxError"
console.log(e.message); // "Unexpected end of input"
}
TypeError
The most common error in JavaScript. Thrown when a value is not the type expected:
// Calling a non-function
try {
const x = 5;
x();
} catch (e) {
console.log(e.name); // "TypeError"
console.log(e.message); // "x is not a function"
}
// Accessing property of null/undefined
try {
null.property;
} catch (e) {
console.log(e.name); // "TypeError"
console.log(e.message); // "Cannot read properties of null (reading 'property')"
}
// Assigning to a constant
try {
const PI = 3.14;
PI = 3.15;
} catch (e) {
console.log(e.name); // "TypeError"
console.log(e.message); // "Assignment to constant variable."
}
// Calling new on a non-constructor
try {
const arrow = () => {};
new arrow();
} catch (e) {
console.log(e.name); // "TypeError"
console.log(e.message); // "arrow is not a constructor"
}
ReferenceError
Thrown when accessing a variable that does not exist:
try {
console.log(undeclaredVariable);
} catch (e) {
console.log(e.name); // "ReferenceError"
console.log(e.message); // "undeclaredVariable is not defined"
}
// Also thrown when accessing let/const before declaration (TDZ)
try {
console.log(x);
let x = 5;
} catch (e) {
console.log(e.name); // "ReferenceError"
console.log(e.message); // "Cannot access 'x' before initialization"
}
RangeError
Thrown when a value is outside the allowed range:
// Invalid array length
try {
const arr = new Array(-1);
} catch (e) {
console.log(e.name); // "RangeError"
console.log(e.message); // "Invalid array length"
}
// Stack overflow from infinite recursion
try {
function infinite() { infinite(); }
infinite();
} catch (e) {
console.log(e.name); // "RangeError"
console.log(e.message); // "Maximum call stack size exceeded"
}
// Invalid toFixed precision
try {
(1.5).toFixed(200);
} catch (e) {
console.log(e.name); // "RangeError"
console.log(e.message); // "toFixed() digits argument must be between 0 and 100"
}
URIError
Thrown by URI-related functions when given malformed URIs:
try {
decodeURIComponent("%");
} catch (e) {
console.log(e.name); // "URIError"
console.log(e.message); // "URI malformed"
}
try {
decodeURI("%E0%A4%A");
} catch (e) {
console.log(e.name); // "URIError"
}
EvalError
Historically thrown by eval(). In modern JavaScript, eval throws other error types instead. EvalError exists mainly for backward compatibility. You will rarely see it in practice:
// You can create one manually
const err = new EvalError("eval failed");
console.log(err.name); // "EvalError"
// But eval() itself throws SyntaxError, TypeError, etc.
try {
eval("invalid code +++");
} catch (e) {
console.log(e.name); // "SyntaxError" (not EvalError)
}
AggregateError
Added in ES2021, used by Promise.any() when all promises reject:
try {
throw new AggregateError(
[new Error("first"), new Error("second")],
"All failed"
);
} catch (e) {
console.log(e.name); // "AggregateError"
console.log(e.message); // "All failed"
console.log(e.errors); // [Error: "first", Error: "second"]
}
Inheritance Hierarchy
All built-in errors share the same prototype chain:
Error.prototype → Object.prototype → null
▲
│
├── SyntaxError.prototype
├── TypeError.prototype
├── ReferenceError.prototype
├── RangeError.prototype
├── URIError.prototype
├── EvalError.prototype
└── AggregateError.prototype
You can check error types with instanceof:
try {
null.prop;
} catch (e) {
console.log(e instanceof TypeError); // true
console.log(e instanceof Error); // true (all errors inherit from Error)
console.log(e instanceof RangeError); // false
}
Optional catch Binding
Since ES2019, you can omit the error parameter in the catch clause if you do not need it:
// Before ES2019 had to declare the variable even if unused
try {
// some operation
} catch (unusedError) {
// handle error without using the error object
console.log("An error occurred");
}
// ES2019+ parameter is optional
try {
// some operation
} catch {
// handle error without the error object
console.log("An error occurred");
}
This is useful when you only care that an error happened, not what the error is:
function isValidJSON(str) {
try {
JSON.parse(str);
return true;
} catch {
return false;
}
}
console.log(isValidJSON('{"name": "Alice"}')); // true
console.log(isValidJSON("not json")); // false
console.log(isValidJSON("")); // false
// Feature detection
let supportsFeature = false;
try {
// Try to use a feature
new SharedArrayBuffer(1024);
supportsFeature = true;
} catch {
// Feature not available: we don't need the error details
supportsFeature = false;
}
Use optional catch binding only when you genuinely do not need the error object. In most cases, you should capture the error for logging, debugging, or conditional handling.
The throw Operator: Throwing Custom Errors
The throw operator lets you create and throw your own errors. Technically, you can throw any value, but you should always throw Error objects (or instances of Error subclasses) for consistency and to get a stack trace.
Basic Syntax
throw new Error("Something went wrong");
When throw executes, the current function stops immediately, and control is transferred to the nearest catch block up the call stack. If no catch block is found, the program terminates.
Throwing Error Objects
function divide(a, b) {
if (b === 0) {
throw new Error("Division by zero");
}
return a / b;
}
try {
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // Throws!
} catch (e) {
console.log(e.message); // "Division by zero"
}
Using Specific Error Types
Choose the error type that best describes the problem:
function setAge(age) {
if (typeof age !== "number") {
throw new TypeError(`Age must be a number, got ${typeof age}`);
}
if (age < 0 || age > 150) {
throw new RangeError(`Age must be between 0 and 150, got ${age}`);
}
return age;
}
try {
setAge("twenty");
} catch (e) {
console.log(e.name); // "TypeError"
console.log(e.message); // "Age must be a number, got string"
}
try {
setAge(-5);
} catch (e) {
console.log(e.name); // "RangeError"
console.log(e.message); // "Age must be between 0 and 150, got -5"
}
You Can Throw Anything (But You Should Not)
JavaScript lets you throw any value: strings, numbers, objects, even undefined. But doing so loses the stack trace and makes error handling inconsistent:
// These all work but are BAD PRACTICE
try {
throw "just a string"; // No stack trace, no name property
} catch (e) {
console.log(e); // "just a string"
console.log(e.stack); // undefined (no stack trace!)
console.log(e.message); // undefined (no message property!)
console.log(e instanceof Error); // false (not an Error object)
}
try {
throw 404; // A number
} catch (e) {
console.log(e); // 404
}
try {
throw { code: "INVALID", details: "bad input" }; // Plain object
} catch (e) {
console.log(e.stack); // undefined (no stack trace)
}
Always throw Error objects:
// GOOD: always throw Error instances
throw new Error("Something failed");
throw new TypeError("Expected a string");
throw new RangeError("Value out of bounds");
Validating Input with throw
A common pattern is validating data and throwing descriptive errors:
function processUser(userData) {
if (!userData) {
throw new Error("User data is required");
}
if (typeof userData !== "object") {
throw new TypeError("User data must be an object");
}
if (!userData.name) {
throw new Error("User name is required");
}
if (typeof userData.name !== "string") {
throw new TypeError("User name must be a string");
}
if (!userData.email) {
throw new Error("User email is required");
}
if (!userData.email.includes("@")) {
throw new Error(`Invalid email format: ${userData.email}`);
}
return {
name: userData.name.trim(),
email: userData.email.toLowerCase().trim()
};
}
// Test with various inputs
const testCases = [
null,
"not an object",
{},
{ name: 42 },
{ name: "Alice" },
{ name: "Alice", email: "not-email" },
{ name: "Alice", email: "alice@example.com" }
];
for (const input of testCases) {
try {
const result = processUser(input);
console.log("Success:", result);
} catch (e) {
console.log(`[${e.name}] ${e.message}`);
}
}
Output:
[Error] User data is required
[TypeError] User data must be an object
[Error] User name is required
[TypeError] User name must be a string
[Error] User email is required
[Error] Invalid email format: not-email
Success: { name: 'Alice', email: 'alice@example.com' }
Rethrowing: Handling Only Known Errors
A catch block receives all errors that occur in the try block. But you usually want to handle only specific types of errors and let unexpected ones propagate to a higher-level handler. This pattern is called rethrowing.
The Problem: Catching Too Much
function readConfig(jsonString) {
try {
const config = JSON.parse(jsonString);
// Bug: 'userName' should be 'config.name'
return userName.toUpperCase(); // ReferenceError (unintended bug!)
} catch (e) {
// This catches EVERYTHING, including bugs we didn't expect
console.log("Invalid JSON config");
return null;
}
}
const result = readConfig('{"name": "Alice"}');
console.log(result); // null (but the JSON was valid! We hid a real bug)
The catch block assumed the error was a JSON parsing problem, but it was actually a ReferenceError caused by a typo. The bug was silently swallowed and will be very hard to find.
The Solution: Check the Error Type and Rethrow
function readConfig(jsonString) {
try {
const config = JSON.parse(jsonString);
return config.name.toUpperCase();
} catch (e) {
// Handle only the errors we expect
if (e instanceof SyntaxError) {
console.log("Invalid JSON:", e.message);
return null;
}
// Rethrow everything else (don't hide bugs)
throw e;
}
}
// Valid JSON: works fine
console.log(readConfig('{"name": "Alice"}')); // "ALICE"
// Invalid JSON: caught and handled
console.log(readConfig("not json")); // "Invalid JSON: ..." → null
// Missing property: TypeError propagates up (not silenced)
try {
readConfig('{}'); // config.name is undefined, .toUpperCase() throws TypeError
} catch (e) {
console.log("Unexpected error:", e.name, e.message);
// "Unexpected error: TypeError Cannot read properties of undefined (reading 'toUpperCase')"
}
Now the function handles JSON parsing errors gracefully but lets other errors (which indicate real bugs) propagate to the caller.
A More Complete Rethrowing Example
function fetchUserData(userId) {
try {
// Simulate various possible errors
if (userId < 0) {
throw new RangeError("User ID must be positive");
}
if (userId === 0) {
throw new TypeError("User ID cannot be zero");
}
// Simulate a network response
const response = '{"id": 1, "name": "Alice"}';
const data = JSON.parse(response);
return data;
} catch (e) {
if (e instanceof SyntaxError) {
// Handle malformed JSON from the server
console.error("Server returned invalid JSON:", e.message);
return null;
}
if (e instanceof RangeError) {
// Handle invalid input
console.error("Invalid input:", e.message);
return null;
}
// TypeError or any other error: rethrow
// These indicate programming errors that should not be hidden
throw e;
}
}
// Normal use
console.log(fetchUserData(1)); // { id: 1, name: "Alice" }
// Handled errors
console.log(fetchUserData(-1)); // "Invalid input: ..." → null
// Unhandled errors propagate
try {
fetchUserData(0); // TypeError is rethrown
} catch (e) {
console.log("Caller caught:", e.name, e.message);
}
The rethrowing pattern is essential for robust error handling. A catch block that catches everything without checking the error type is a bug magnet. Always ask yourself: "What specific errors do I expect here?" Handle those, and rethrow everything else.
The try...catch...finally Statement
The finally clause adds a block that runs no matter what happens: whether the try block succeeds, whether an error is thrown, or whether an error is caught. It is the place for cleanup code that must execute regardless of the outcome.
Basic Syntax
try {
// Code that might throw
} catch (e) {
// Error handling
} finally {
// ALWAYS runs (cleanup goes here)
}
finally Runs in All Cases
function testFinally(shouldThrow) {
try {
console.log("try: starting");
if (shouldThrow) {
throw new Error("boom");
}
console.log("try: finished successfully");
} catch (e) {
console.log("catch:", e.message);
} finally {
console.log("finally: always runs");
}
}
console.log("--- No error ---");
testFinally(false);
// try: starting
// try: finished successfully
// finally: always runs
console.log("\n--- With error ---");
testFinally(true);
// try: starting
// catch: boom
// finally: always runs
try...finally Without catch
You can use finally without catch. The error is not handled (it propagates), but the finally block still runs:
function riskyOperation() {
const resource = acquireResource();
try {
// Work with the resource
processResource(resource);
} finally {
// Always release the resource, even if processResource throws
releaseResource(resource);
}
}
A practical example with a timer:
function measureTime(fn) {
const start = performance.now();
try {
return fn();
} finally {
const elapsed = performance.now() - start;
console.log(`Execution time: ${elapsed.toFixed(2)}ms`);
}
}
// Even if fn throws, we still log the time
try {
measureTime(() => {
throw new Error("failed");
});
} catch (e) {
console.log("Caught:", e.message);
}
// Execution time: 0.05ms
// Caught: failed
Real-World Use Case: Cleanup
function processFile(filename) {
let fileHandle = null;
try {
fileHandle = openFile(filename); // Might throw if file doesn't exist
const content = readFile(fileHandle); // Might throw if read fails
const result = parseContent(content); // Might throw if parsing fails
return result;
} catch (e) {
console.error(`Error processing ${filename}:`, e.message);
return null;
} finally {
// Always close the file, whether we succeeded or failed
if (fileHandle) {
closeFile(fileHandle);
console.log("File handle closed");
}
}
}
Loading State Management
A common UI pattern:
async function loadData() {
const spinner = document.getElementById("spinner");
try {
spinner.style.display = "block"; // Show loading spinner
const response = await fetch("/api/data");
const data = await response.json();
displayData(data);
} catch (e) {
displayError("Failed to load data");
} finally {
spinner.style.display = "none"; // ALWAYS hide spinner
}
}
finally and return: finally Always Runs
The finally block runs even when try or catch contain return, throw, break, or continue statements. This is a strong guarantee, but it leads to some surprising behavior.
finally Runs After return
function getValue() {
try {
return "from try";
} finally {
console.log("finally runs even after return");
}
}
const result = getValue();
console.log(result);
// "finally runs even after return"
// "from try"
The return in try sets the return value, but finally executes before the function actually returns.
finally Can Override return
If finally has its own return, it overrides whatever try or catch returned:
function overriddenReturn() {
try {
return "from try";
} finally {
return "from finally"; // This overrides the try's return!
}
}
console.log(overriddenReturn()); // "from finally"
function anotherExample() {
try {
throw new Error("problem");
} catch (e) {
return "from catch";
} finally {
return "from finally"; // Overrides catch's return too!
}
}
console.log(anotherExample()); // "from finally"
Avoid putting return statements in finally blocks. It overrides returns from both try and catch, and it even swallows errors. If try throws an error and catch does not exist (or rethrows), a return in finally will suppress that error entirely:
function swallowedError() {
try {
throw new Error("This error disappears!");
} finally {
return "finally's return swallowed the error";
}
}
// No error is thrown (it was silently suppressed!)
console.log(swallowedError()); // "finally's return swallowed the error"
This is almost always a bug. Use finally for cleanup, not for returning values.
finally Runs Even with Uncaught Errors
function noHandler() {
try {
throw new Error("uncaught!");
} finally {
console.log("finally still runs"); // This executes
}
// The error propagates after finally
}
try {
noHandler();
} catch (e) {
console.log("Outer catch:", e.message);
}
// "finally still runs"
// "Outer catch: uncaught!"
Execution Order Summary
function completeExample() {
console.log("1: before try");
try {
console.log("2: try start");
throw new Error("test");
console.log("3: try end (never reached)");
} catch (e) {
console.log("4: catch");
} finally {
console.log("5: finally");
}
console.log("6: after try/catch/finally");
}
completeExample();
// 1: before try
// 2: try start
// 4: catch
// 5: finally
// 6: after try/catch/finally
Global Error Handlers: window.onerror
When an error is thrown and no try...catch anywhere in the call stack catches it, it becomes an unhandled error. Browsers and Node.js provide global handlers as a last resort to detect these errors.
window.onerror (Browser)
The window.onerror handler catches unhandled errors globally:
window.onerror = function(message, source, lineno, colno, error) {
console.log("Global error caught!");
console.log("Message:", message);
console.log("Source:", source);
console.log("Line:", lineno);
console.log("Column:", colno);
console.log("Error object:", error);
// Return true to prevent the default browser error logging
// Return false (or nothing) to let it also appear in the console
return true;
};
// This error is not caught by any try...catch
undeclaredVariable; // Caught by window.onerror
Parameters:
| Parameter | Description |
|---|---|
message | The error message string |
source | URL of the script that caused the error |
lineno | Line number where the error occurred |
colno | Column number where the error occurred |
error | The Error object (may be null for cross-origin scripts) |
window.addEventListener("error")
The modern alternative to window.onerror:
window.addEventListener("error", function(event) {
console.log("Error event:", event.message);
console.log("Filename:", event.filename);
console.log("Line:", event.lineno);
console.log("Column:", event.colno);
console.log("Error:", event.error);
event.preventDefault(); // Prevents default console error
});
Unhandled Promise Rejections
window.onerror does not catch unhandled Promise rejections. There is a separate event for those:
window.addEventListener("unhandledrejection", function(event) {
console.log("Unhandled promise rejection:", event.reason);
// Prevent the default handling (console error)
event.preventDefault();
});
// This rejection is NOT caught by window.onerror
Promise.reject(new Error("Promise failed"));
// Neither is this
async function failing() {
throw new Error("Async failure");
}
failing(); // Unhandled rejection
Node.js Global Handlers
In Node.js, the equivalent handlers are on the process object:
// Uncaught exceptions
process.on("uncaughtException", (error) => {
console.error("Uncaught exception:", error.message);
// Log the error, then exit (continuing after uncaughtException is unsafe)
process.exit(1);
});
// Unhandled promise rejections
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled rejection:", reason);
});
Practical Error Reporting
In production applications, global handlers are used to send errors to monitoring services:
window.onerror = function(message, source, lineno, colno, error) {
// Send to error tracking service
const errorReport = {
message,
source,
lineno,
colno,
stack: error ? error.stack : "No stack trace",
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: new Date().toISOString()
};
// Send asynchronously (use navigator.sendBeacon for reliability)
navigator.sendBeacon("/api/errors", JSON.stringify(errorReport));
return false; // Still show in console during development
};
window.addEventListener("unhandledrejection", function(event) {
const errorReport = {
type: "unhandled_rejection",
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack || "No stack trace",
timestamp: new Date().toISOString()
};
navigator.sendBeacon("/api/errors", JSON.stringify(errorReport));
});
Global error handlers are a safety net, not a replacement for proper try...catch error handling. They catch errors that slipped through your defenses. In a well-written application, most errors should be caught and handled at the appropriate level, with global handlers serving as a last resort for unexpected failures.
Summary
Error handling is not optional in production JavaScript. The try...catch...finally statement gives you precise control over how your program responds to runtime failures.
| Concept | Key Point |
|---|---|
| Syntax errors | Happen during parsing, before code runs. try...catch cannot catch them. |
| Runtime errors | Happen during execution. try...catch is designed for these. |
try...catch | Wraps risky code. Errors jump to catch, skipping remaining try code. |
| Synchronous only | try...catch does not catch errors in setTimeout callbacks, event handlers, or other async code. |
| Error object | Has name, message, and stack properties. |
| Built-in types | Error, SyntaxError, TypeError, ReferenceError, RangeError, URIError, EvalError, AggregateError. |
| Optional catch binding | catch { } without parameter (ES2019+). |
throw | Creates and throws errors. Always throw Error objects, not primitives. |
| Rethrowing | Catch only expected error types, rethrow everything else. |
finally | Always runs, regardless of success, error, or return. Use for cleanup. |
finally + return | return in finally overrides try/catch returns and swallows errors. Avoid. |
window.onerror | Global handler for uncaught runtime errors in browsers. |
unhandledrejection | Global handler for uncaught Promise rejections. Safety net, not a replacement for proper handling. |
Key rules to remember:
- Always throw
Errorobjects (or subclasses), never strings or plain objects - Never write a
catchblock that silently swallows all errors. Check the error type and rethrow unknowns. - Use
finallyfor cleanup code that must run regardless of outcome - Never put
returnin afinallyblock - Put
try...catchinside async callbacks, not around them - Use global error handlers as a safety net for logging and monitoring, not as your primary error handling strategy