⚡

JavaScript Error Handling: try/catch and Custom Errors

JavaScript3 min readUpdated October 2026

Learn how to catch, throw, and design errors in JavaScript, including custom error classes, finally, and the async mistakes that let failures slip through.

Every program fails sometimes. A user types something strange, a server goes down, a file is missing. Good error handling isn't about preventing every failure. It's about failing in a way that's clear, safe, and fixable.

try, catch, finally

Wrap risky code in try. If something throws, JavaScript jumps to catch. Code in finally runs either way.

function parseSettings(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    console.log("Bad settings:", error.name);
    return {};
  } finally {
    console.log("parse attempt finished");
  }
}

console.log(parseSettings('{"theme":"dark"}'));
console.log(parseSettings("{oops"));
parse attempt finished
{ theme: 'dark' }
Bad settings: SyntaxError
parse attempt finished
{}

finally is the right place for cleanup: closing a connection, hiding a loading spinner, releasing a lock.

Throwing your own errors

Use throw when something is wrong and the current function can't continue. Always throw an Error object, not a string, because an Error carries a stack trace.

function withdraw(balance, amount) {
  if (typeof amount !== "number" || Number.isNaN(amount)) {
    throw new TypeError("amount must be a number");
  }
  if (amount > balance) {
    throw new RangeError(`can't withdraw ${amount} from ${balance}`);
  }
  return balance - amount;
}

try {
  withdraw(50, 80);
} catch (error) {
  console.log(error.name + ": " + error.message);
}
RangeError: can't withdraw 80 from 50

The built-in types (TypeError, RangeError, SyntaxError) already describe common problems. Use them where they fit.

Custom error classes

When your app has its own failure types, make classes for them. Then callers can tell the difference between "the user typed bad data" and "the server exploded."

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

function validateAge(age) {
  if (!Number.isInteger(age) || age < 0) {
    throw new ValidationError("Age must be a positive whole number", "age");
  }
  return age;
}

try {
  validateAge(-4);
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Fix the "${error.field}" field: ${error.message}`);
  } else {
    throw error;
  }
}
Fix the "age" field: Age must be a positive whole number

Notice the else { throw error; }. Only handle errors you understand. Re-throw the rest so they don't disappear.

Catch where you can do something useful

A common mistake is wrapping everything in try/catch and logging the message. That hides problems. Ask yourself: at this level, can I actually recover?

  • Yes: use a default value, retry, or show a friendly message.
  • No: let the error bubble up to a place that can handle it, such as a top-level handler.
function loadConfig(text) {
  // No try/catch here. Callers decide what a bad config means.
  return JSON.parse(text);
}

function startApp(text) {
  try {
    const config = loadConfig(text);
    console.log("Started with", config);
  } catch {
    console.log("Config was invalid, using defaults");
  }
}

startApp("not json");
Config was invalid, using defaults

If you don't need the error object, you can leave off the (error) part entirely, as shown above.

Errors in async code

A try/catch only catches errors that happen while it's running. It works with await, because the code pauses inside the try:

async function fetchUser() {
  throw new Error("network down");
}

async function main() {
  try {
    const user = await fetchUser();
    console.log(user);
  } catch (error) {
    console.log("Caught:", error.message);
  }
}

main();
Caught: network down

Here's where it goes wrong. Without await, the promise is created inside the try, but the failure happens later, after the try has already finished:

try {
  fetchUser();          // missing await
} catch (error) {
  console.log("never runs");
}

For promise chains, add .catch() at the end. For code you don't await, attach a handler directly: fetchUser().catch(console.error).

Fetch needs an extra check

fetch only rejects when the network fails. A 404 or 500 response counts as success as far as the promise is concerned.

async function getJson(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }
  return response.json();
}

Always check response.ok. This one line prevents a lot of "why is my data undefined?" mysteries.

Add context with error cause

When you catch an error and throw a new one, keep the original attached using the cause option.

function readUser(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    throw new Error("Could not read user data", { cause: error });
  }
}

try {
  readUser("{bad");
} catch (error) {
  console.log(error.message);
  console.log("because:", error.cause.name);
}
Could not read user data
because: SyntaxError

The high-level message tells you what went wrong in your app's terms. The cause keeps the low-level detail for debugging.

Mistakes to avoid

  • Empty catch blocks. catch {} silently swallows problems. At least log something.
  • Showing raw error messages to users. Show something friendly, and log the technical details somewhere you can read them.
  • Throwing strings. throw "oops" has no stack trace. Use new Error("oops").
  • Using exceptions for normal flow. If a failure is expected and common, like a search with no results, return an empty result instead of throwing.

Key takeaways

  • try for risky code, catch to recover, finally for cleanup.
  • Throw Error objects, and make custom classes for your app's own failure types.
  • Only catch what you can handle. Re-throw the rest.
  • With async code, use await inside try, and check response.ok with fetch.

Try it yourself

Reading helps, but typing the code is what makes it stick. Open the editor, paste any example from this article, and change it until it breaks.

Open the JavaScript editor

Related Articles