JavaScript · Intermediate
Error Handling in JavaScript
In short: JavaScript signals failure by throwing a value, normally an Error object, which unwinds the call stack until a try block's catch clause receives it. finally runs cleanup whether or not anything was thrown, built-in types such as TypeError and RangeError describe what went wrong, and a class that extends Error lets callers recognise your own failures with instanceof.
Throwing, catching and cleaning up
When a program cannot continue, it throws: throw new RangeError("port out of range"). Execution leaves the current function at once, then the calling function, and so on up the stack until a try block is found whose catch clause takes over. If nothing catches it, the program stops with an uncaught error. The engine throws in the same way when it meets a problem of its own: undefined.length throws a TypeError, JSON.parse("{oops") throws a SyntaxError, and reading a variable that does not exist throws a ReferenceError.
try { ... } catch (err) { ... } marks the code that may fail and the code that handles the failure. The catch parameter receives whatever was thrown. Keep the try block small: if it wraps twenty lines, the handler cannot tell which one failed, and a bug you did not anticipate is handled as though it were the failure you expected. Since ES2019 the parameter is optional, so catch { ... } is legal when you do not need the error object.
finally runs after the try and catch blocks whatever happened: on success, after a caught error, after an uncaught error on its way out, and even when the try block executes return. That guarantee makes it the place for cleanup such as closing a file, releasing a lock or hiding a spinner. A return inside finally overrides the earlier result, so avoid that.
Any value can be thrown, but always throw an Error object. Error and its subclasses carry a message, a name that identifies the type, and a stack trace pointing at where the throw happened; a thrown string has none of these and instanceof checks cannot recognise it. The built-in types tell the reader what kind of problem occurred: TypeError for a wrong kind of value, RangeError for a value outside the allowed range, SyntaxError for unparsable text. ES2022 added the cause option, new Error("config failed", { cause: err }), which keeps the original error attached when you rethrow a more descriptive one.
For failures specific to your program, extend Error. A class such as StockError sets this.name in its constructor and can add fields like the item concerned. A handler then checks err instanceof StockError and deals with that case, and rethrows anything else with throw err so a genuine bug still surfaces. Test the type, not the message text: messages are for humans and change.
One boundary matters: try/catch only sees errors thrown synchronously inside its block. An error inside a callback that runs later, such as a timer or an event handler, happens after the try has finished and is not caught by it. Errors in promise-based code are caught with .catch() or with try/catch around await, which the promises and async/await lessons cover.
Syntax
try {
mayFail();
} catch (err) {
if (err instanceof RangeError) { /* handle this kind */ }
else { throw err; } // not ours: rethrow
} finally {
cleanUp(); // always runs
}
throw new TypeError("qty must be a number");
throw new Error("could not load config", { cause: original }); // ES2022
class StockError extends Error {
constructor(message, item) {
super(message);
this.name = "StockError";
this.item = item;
}
}catch without a parameter, catch { ... }, is allowed since ES2019. Every Error has name, message and (in practice) stack.
Skipping bad lines instead of crashing
Each input line should be a JSON object with an item and a numeric qty. Invalid JSON and a wrong type are reported and skipped; the loop keeps going.
let line;
let loaded = 0;
while ((line = readline()) !== null) {
try {
const entry = JSON.parse(line);
if (typeof entry.qty !== "number") {
throw new TypeError("qty must be a number");
}
loaded += 1;
console.log(`ok: ${entry.item} x ${entry.qty}`);
} catch (err) {
if (err instanceof SyntaxError) {
console.log("skipped: not valid JSON");
} else {
console.log(`skipped: ${err.message}`);
}
}
}
console.log(`${loaded} entries loaded`);Input given to the program: {"item":"nails","qty":200} ↵ {"item":"glue","qty":"two"} ↵ {item: hammer} ↵ {"item":"tape","qty":3}
Output
ok: nails x 200 skipped: qty must be a number skipped: not valid JSON ok: tape x 3 2 entries loaded
JSON.parse throws a SyntaxError on the third line, and the program's own throw new TypeError handles the second. Both land in the same catch, which tells them apart with instanceof. The SyntaxError message is generated by the engine and worded differently in different browsers, so the program prints its own wording instead of err.message for that case. Because the try sits inside the loop, one bad line never stops the others.
A custom error class, rethrowing and finally
take throws StockError for business failures and TypeError for misuse. attempt handles only StockError, rethrows everything else, and always prints a closing line.
class StockError extends Error {
constructor(message, item) {
super(message);
this.name = "StockError";
this.item = item;
}
}
const stock = { paint: 3, brush: 0 };
function take(item, qty) {
if (typeof qty !== "number") throw new TypeError("qty must be a number");
if (!(item in stock)) throw new StockError("unknown item", item);
if (stock[item] < qty) throw new StockError("not enough", item);
stock[item] -= qty;
return stock[item];
}
function attempt(item, qty) {
try {
const left = take(item, qty);
console.log(`took ${qty} ${item}, ${left} left`);
} catch (err) {
if (err instanceof StockError) {
console.log(`${err.name}: ${err.message} (${err.item})`);
} else {
throw err;
}
} finally {
console.log("request handled");
}
}
attempt("paint", 2);
attempt("brush", 1);
attempt("tape", 1);
try {
attempt("paint", "2");
} catch (err) {
console.log("outer caught", err.name);
}Output
took 2 paint, 1 left request handled StockError: not enough (brush) request handled StockError: unknown item (tape) request handled request handled outer caught TypeError
StockError carries an extra item field, so the handler can print which product failed without parsing the message. The fourth call passes a string quantity; that is a TypeError, which attempt does not own, so it rethrows. Look at the order of the last two lines: finally printed request handled before the error reached the outer catch, because finally runs as the error leaves the function.
finally with return, the cause option and why not to throw strings
parsePort returns from inside try and still runs finally, loadConfig wraps a low-level error in a descriptive one, and the last block shows what a thrown string lacks.
function parsePort(text) {
try {
const n = Number(text);
if (!Number.isInteger(n) || n < 1 || n > 65535) {
throw new RangeError(`invalid port: ${text}`);
}
return n;
} finally {
console.log(`checked "${text}"`);
}
}
console.log(parsePort("8080"));
try {
parsePort("99999");
} catch (err) {
console.log(err.name, "-", err.message);
}
function loadConfig(text) {
try {
return JSON.parse(text);
} catch (err) {
throw new Error("config could not be loaded", { cause: err });
}
}
try {
loadConfig("{bad");
} catch (err) {
console.log(err.message);
console.log("cause:", err.cause.name);
}
try {
throw "oops";
} catch (err) {
console.log(typeof err, err instanceof Error);
}Output
checked "8080" 8080 checked "99999" RangeError - invalid port: 99999 config could not be loaded cause: SyntaxError string false
checked "8080" appears before 8080 because finally runs on the way out of the return, before the caller receives the value. loadConfig converts a parser error into one that says what the program was trying to do, and cause keeps the original available for logging. The thrown string arrives in catch as a plain string: no name, no message, no stack, and instanceof Error is false, so nothing can classify it.
Built-in error types
| Type | Thrown when | Typical trigger |
|---|---|---|
| TypeError | a value is of the wrong kind or used wrongly | null.name, calling a non-function, x is not iterable |
| RangeError | a number is outside the allowed range | new Array(-1), (1.5).toFixed(200) |
| SyntaxError | text cannot be parsed | JSON.parse("{bad") |
| ReferenceError | a name does not exist | reading an undeclared variable, let before its line |
| URIError | a URI function gets malformed input | decodeURIComponent("%") |
| Error | the general base type | your own throw new Error(...) |
Common mistakes
Catching everything and doing nothing
Why it goes wrong: An empty
catch {}hides the failure. The program continues with missing or wrong data and the real cause is invisible when something breaks later.Fix: Handle the cases you understand, and rethrow or at least report the ones you do not.
JavaScript · fixcatch (err) { if (!(err instanceof StockError)) throw err; console.log(err.message); }Throwing a string or a plain object
Why it goes wrong:
throw "bad input"gives the catcher noname, no stack trace and nothinginstanceofcan test, so handlers fall back to comparing text.Fix: Throw
new Error(...), a built-in subtype or your own class that extendsError.Deciding what to do by matching the message text
Why it goes wrong: Engine messages differ between browsers and versions, and your own messages change when you edit them.
if (err.message === "not enough")breaks silently.Fix: Branch on the type with
instanceof, or on a field you set yourself such aserr.code.Expecting try/catch to catch errors from later callbacks
Why it goes wrong:
try { setTimeout(() => { throw new Error("late"); }, 0); } catch {}catches nothing: thetryblock finished long before the callback ran.Fix: Put the
try/catchinside the callback, or use promises andawaitso the error flows back to acatchthat is still active.
Where you use this
Input from outside the program is the first customer: form fields, uploaded files, query strings and API responses arrive malformed sooner or later. Wrapping the parse of each record in try/catch lets a batch job report the bad record and continue, instead of dying on the first one. At the boundary of a library or a service, a small set of custom error classes tells callers precisely what failed, so a payment module can throw CardDeclined and InsufficientFunds and let the caller show two different messages while any unexpected TypeError is left to propagate as the bug it is.
finally earns its place around anything that must be undone: a loading indicator that must be hidden, a temporary file that must be deleted, a counter of in-flight requests that must be decremented. Putting that code in finally means it happens on the failure path too, without repeating it in every branch.
showSpinner();
try {
render(parse(text));
} catch (err) {
showMessage(err instanceof SyntaxError ? "File is not valid JSON" : "Could not load file");
} finally {
hideSpinner();
}Key points
throwunwinds the stack until acatchreceives the thrown value; uncaught errors stop the program.- Always throw
Errorobjects: they carryname,messageand a stack trace. - Keep
tryblocks small and branch oninstanceof, never on message text. - Rethrow what you do not handle so genuine bugs stay visible.
finallyalways runs, even afterreturn, and is the place for cleanup.class MyError extends Errorwiththis.nameset gives callers something precise to catch.try/catchsees only synchronous throws; callbacks and promises need their own handling.
Try it yourself
Make divide throw a RangeError with the message cannot divide by zero when b is 0, and catch it where divide is called so the program prints error: followed by the message instead of Infinity.
function divide(a, b) {
return a / b;
}
const [x, y] = readline().split(" ").map(Number);
console.log(divide(x, y));10 0error: cannot divide by zerofunction divide(a, b) {
if (b === 0) throw new RangeError("cannot divide by zero");
return a / b;
}
const [x, y] = readline().split(" ").map(Number);
try {
console.log(divide(x, y));
} catch (err) {
console.log("error: " + err.message);
}Practise this
Open the JavaScript playground
Related lessons
- Classes in JavaScriptHow JavaScript classes work: constructors and this, methods on the prototype, getters, static members, extends and super, and private fields with #.11 min
- JavaScript Functions: Declarations, Arrows and Return ValuesHow to define and call JavaScript functions with declarations, expressions and arrow functions; parameters, default values, return and local scope.10 min
- Conditions in JavaScript: if, else if, else and switchHow if, else if, else and switch choose what runs in JavaScript, which values count as true, and how to write conditions that do what you meant.8 min
Frequently asked questions
What is the difference between throw and return?
return hands a value to the direct caller and execution continues normally there. throw abandons the current function and every caller above it until a catch is found, skipping all the code in between. Use return for expected outcomes, including "not found" when that is normal, and throw for situations the caller cannot proceed from.
Does finally run if there is a return inside try?
Yes. The finally block runs after the return expression has been evaluated and before the caller receives the value. It also runs when an error passes through uncaught. If finally itself contains a return, that value replaces the earlier one and swallows any pending error, which is why returning from finally is discouraged.
How do I create a custom error type in JavaScript?
Declare a class that extends Error, call super(message) in its constructor and set this.name to the class name so logs and String(err) show it. Add any fields the handler will need, such as an item id or a status code. Catchers then test err instanceof YourError.
How this page was checked. Every program on it was run with Node.js 22 at build time; runs in your browser in an isolated Web Worker by the publishing checks, and the output shown is what it printed. The Run buttons execute the same code on your device; nothing you type is sent anywhere.