Exception Handling
|
This section documents modern ECMAScript and core browser JavaScript APIs — it is not tied to any specific framework or library (React, Vue, Angular, etc.). This content was generated with the assistance of AI and should be verified against the current ECMAScript specification and MDN documentation before relying on it in production, since JavaScript language features and browser API support continue to evolve. This section’s bibliography lists the reference material consulted while preparing these pages. |
Exceptions are JavaScript’s mechanism for signaling and reacting to failure — an operation that cannot complete
normally interrupts its own execution and hands control to whichever surrounding code is prepared to deal with
it. This page covers throw, the full try/catch/finally mechanics, defining custom exception types, and
how exceptions behave in asynchronous code. For the built-in Error class hierarchy itself (TypeError,
RangeError, and the rest), see
Standard Library: Dates, Errors & JSON.
throw
throw immediately stops normal execution of the current function and propagates a value — any value at all — up to the nearest enclosing exception handler:
function requirePositive(n) {
if (n <= 0) {
throw new Error(`expected a positive number, got ${n}`);
}
return n;
}
throw accepts any expression — a string, a number, a plain object — but throwing an Error (or subclass)
instance is strongly conventional and almost always the right choice: constructing an Error captures the
current call stack at the point of construction (available afterward as .stack), which is often the single
most useful piece of information for diagnosing an exception after the fact. Throwing a bare string or plain
object discards that — and forces every catch block to first check what kind of value it received before it
can safely read a .message or similar.
try / catch / finally
A try block wraps code that might throw; a catch block, if present, runs when something inside try throws;
a finally block, if present, always runs afterward, whether or not an exception occurred:
try {
riskyOperation();
} catch (err) {
console.error("Operation failed:", err.message);
} finally {
cleanup(); // always runs
}
catch’s binding (`err above) is optional — write a bare catch { when the handler doesn’t need the actual
exception value, for example when any failure is handled identically regardless of what went wrong:
try {
return JSON.parse(text);
} catch {
return null; // any parse failure just falls back to null
}
finally always runs
finally is guaranteed to run in every case: after try completes normally, after catch handles an exception
(caught or not), and even when neither block completes normally — including when an exception inside try is
never caught at all (no catch, or a catch that itself throws), and when try or catch executes a return,
break, or continue. This makes finally the correct place for cleanup that must happen unconditionally — releasing a lock, closing a connection, hiding a loading indicator:
function readConfig() {
try {
return JSON.parse(loadConfigText()); // may throw a SyntaxError
} finally {
console.log("config load attempted"); // runs whether parse succeeded, threw, or the throw propagates on
}
}
In the example above, readConfig() has no catch at all — a SyntaxError inside try still runs finally
before propagating on to readConfig()’s own caller. The same guarantee applies to a `return inside try:
function example() {
try {
return "from try";
} finally {
console.log("this still runs before the function actually returns");
}
}
A return (or throw) written inside finally itself overrides whatever try or catch was about to
do — it silently discards a pending return value or a pending exception. This is almost never intentional; avoid
return/throw/break/continue inside finally.
|
Exception Propagation
An exception that isn’t caught by the immediately enclosing try keeps propagating outward, one enclosing scope
at a time, until either a catch block along the way handles it or it reaches the top of the call stack
uncaught — at which point, in a browser, the window.onerror handler runs (if one is registered) and the error
is reported to the console; in Node.js, an uncaught exception terminates the process by default.
outer() and middle() never see it"] E -->|"no"| G{"try/catch inside outer()?"} E -->|"yes"| H["handled inside middle()
outer() never sees it"] G -->|"no"| I["uncaught: reaches top of call stack
(window.onerror / process crash)"] G -->|"yes"| J["handled inside outer()"]
Nesting try/catch blocks works the same way at a finer grain: a catch at an inner level can handle what it
knows how to recover from and re-throw anything else, letting an outer catch handle the parts it’s actually
equipped for:
function parseAndValidate(text) {
let data;
try {
data = JSON.parse(text);
} catch (err) {
throw new Error(`invalid JSON: ${err.message}`); // re-throw as a more specific error
}
if (!data.id) {
throw new Error("missing required field: id");
}
return data;
}
Custom Exception Types
Application code commonly defines its own Error subclasses so a catch block can distinguish what kind of
failure occurred instead of parsing a message string. Subclassing follows the same extends/super() mechanics
as any other class (see Classes):
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError"; // shows up in stack traces and console output instead of "Error"
this.field = field;
}
}
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = "NotFoundError";
}
}
instanceof then lets a catch block branch on the exception’s actual type, handling each failure mode
appropriately instead of treating every exception identically:
function handleRequest(request) {
try {
return processRequest(request);
} catch (err) {
if (err instanceof ValidationError) {
return { status: 400, body: `Invalid ${err.field}: ${err.message}` };
}
if (err instanceof NotFoundError) {
return { status: 404, body: err.message };
}
throw err; // anything else is unexpected -- let it propagate rather than mask it
}
}
Re-throwing anything the catch block doesn’t specifically recognize (the final throw err; above) is
important: swallowing every exception indiscriminately hides genuine bugs and makes them far harder to diagnose
later.
Exceptions in Asynchronous JavaScript
Asynchronous code has two idiomatic error-handling styles, corresponding to the two ways of consuming a
Promise (see Asynchronous JavaScript for the underlying Promise/
async/await mechanics this section assumes as background).
try/catch around await
Inside an async function, await`ing a rejected Promise throws the rejection reason at the point of the
`await — ordinary try/catch catches it exactly as if the failure had happened synchronously:
async function loadUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new NotFoundError(`user ${id} not found (HTTP ${response.status})`);
}
return await response.json();
} catch (err) {
console.error("Failed to load user:", err.message);
throw err; // still rejects the Promise loadUser() returns, for the caller to handle
}
}
.catch() on a Promise chain
Outside an async function — or by choice, even inside one — the same failure can be handled with .catch()
attached to the Promise chain instead:
function loadUser(id) {
return fetch(`/api/users/${id}`)
.then(response => {
if (!response.ok) {
throw new NotFoundError(`user ${id} not found (HTTP ${response.status})`);
}
return response.json();
})
.catch(err => {
console.error("Failed to load user:", err.message);
throw err; // re-throw to keep the returned promise rejected for the caller
});
}
Both versions behave identically to their caller: a rejected fetch(), a non-OK response, or a NotFoundError
thrown from inside either handler all end up rejecting the Promise loadUser() returns, catchable by the caller
with either try/catch around await loadUser(id) or .catch() on loadUser(id). .then()’s optional
second argument (`promise.then(onFulfilled, onRejected)) can also handle rejection, but .catch() is generally
clearer, since a rejection thrown by the first argument still falls through to a later .catch() rather than
`.then()’s own second argument, which only catches rejection of the original promise.
Unhandled promise rejections
A rejected Promise with no .catch() (and no await inside a try that catches it) anywhere in its chain is an
unhandled rejection. In browsers, this fires an unhandledrejection event on window, useful as a
last-resort diagnostic or reporting hook:
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
event.preventDefault(); // suppress the default browser console warning, if handled here instead
});
Relying on unhandledrejection as the primary error-handling strategy is a code smell — it exists as a safety
net for genuinely unexpected failures, not a substitute for a .catch()/try/catch at the point where a
rejection can actually be handled meaningfully.