JavaScript · Intermediate

Error Handling in JavaScript

10 min readUpdated September 24, 2026Every example verified

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

 JavaScript · 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.

 JavaScript
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.

 JavaScript
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.

 JavaScript
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

TypeThrown whenTypical trigger
TypeErrora value is of the wrong kind or used wronglynull.name, calling a non-function, x is not iterable
RangeErrora number is outside the allowed rangenew Array(-1), (1.5).toFixed(200)
SyntaxErrortext cannot be parsedJSON.parse("{bad")
ReferenceErrora name does not existreading an undeclared variable, let before its line
URIErrora URI function gets malformed inputdecodeURIComponent("%")
Errorthe general base typeyour 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 · fix
    catch (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 no name, no stack trace and nothing instanceof can test, so handlers fall back to comparing text.

    Fix: Throw new Error(...), a built-in subtype or your own class that extends Error.

  • 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 as err.code.

  • Expecting try/catch to catch errors from later callbacks

    Why it goes wrong: try { setTimeout(() => { throw new Error("late"); }, 0); } catch {} catches nothing: the try block finished long before the callback ran.

    Fix: Put the try/catch inside the callback, or use promises and await so the error flows back to a catch that 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.

 JavaScript · in practice
showSpinner();
try {
  render(parse(text));
} catch (err) {
  showMessage(err instanceof SyntaxError ? "File is not valid JSON" : "Could not load file");
} finally {
  hideSpinner();
}

Key points

  • throw unwinds the stack until a catch receives the thrown value; uncaught errors stop the program.
  • Always throw Error objects: they carry name, message and a stack trace.
  • Keep try blocks small and branch on instanceof, never on message text.
  • Rethrow what you do not handle so genuine bugs stay visible.
  • finally always runs, even after return, and is the place for cleanup.
  • class MyError extends Error with this.name set gives callers something precise to catch.
  • try/catch sees 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.

Your program
function divide(a, b) {
  return a / b;
}

const [x, y] = readline().split(" ").map(Number);
console.log(divide(x, y));
Input the program receives: 10 0
Expected output: error: cannot divide by zero

Practise this

Open the JavaScript playground

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.

Progress is stored only in this browser.

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.