JavaScript Errors

Error Handling in JavaScript

Things go wrong. Networks drop, inputs surprise you, APIs change. Good error handling keeps those failures from becoming outages and angry users.

intermediate14 min readUpdated Sep 15, 2026
js
// api.js
class HttpError extends Error {
  constructor(status) {
    super(`Request failed with status ${status}`);
    this.name = "HttpError";
    this.status = status;
  }
}

try {
  const res = await fetch("/api/data");
  if (!res.ok) throw new HttpError(res.status);
  const data = await res.json();
} catch (error) {
  if (error instanceof HttpError) report(error.status);
  else console.error(error);
}
Base type
Error
Raise with
throw
Handle with
try / catch / finally
Extend with
class extends Error
Async
try/catch + rejected promises
Safety net
Global error handlers

Hands-on

Try, catch and finally

Change the number to trigger the error and watch the order of messages.

Try, catch and finally

Change the number to trigger the error and watch the order of messages.

Why it matters

Why error handling matters

Fail gracefully

A handled error keeps the app running and shows the user something useful instead of a blank screen.

Debug faster

Good errors carry a message, a type and a stack trace, turning a mystery into a five-minute fix.

Build trust

Clear recovery and honest messages make an app feel solid even when the network is not.

The big picture

Three questions every failure asks

Good error handling answers the same three questions every time: what went wrong, who should know, and what happens next.

Detection

What went wrong

Throw precise, typed errors at the point of failure instead of letting bad state spread.

Recovery

Who handles it

Catch where you can do something meaningful, and let everything else bubble up.

Communication

What happens next

Tell the user what they can do, log the detail for developers, and keep the app alive.

Error handling at a glance

The tools you will use

Error objects

message, name, stack and cause — the built-in shape of a failure.

throw

Raise your own errors at the moment a rule is broken.

try / catch

Wrap risky code and handle the failure without crashing.

finally

Cleanup that runs whether the attempt succeeded or failed.

Custom errors

Subclasses of Error that carry domain-specific data.

Async errors

Rejected promises and try/catch around await.

A short history

How JavaScript learned to fail well

  1. 1995

    Errors on error

    Early JavaScript exposes window.onerror as the only global safety net.

    95
  2. 1999

    try / catch arrives

    ES3 brings structured exception handling to the language.

    99
  3. 2015

    Promises and rejections

    ES6 makes unhandled promise rejections a new class of failure to manage.

    15
  4. 2021

    error.cause

    Errors can now wrap the underlying cause, preserving the original context.

    21
  5. Today

    Error-first culture

    Tooling, linters and frameworks all assume explicit, typed error handling.

    Today

The complete guide

Error Handling in JavaScript: Everything you need to know

Why errors are inevitable

Every program eventually meets a situation it did not expect: a server returns a 500, a user types letters into a numeric field, a file is missing, a third-party API changes shape. Error handling is not about preventing every failure — it is about deciding what happens when one occurs.

Well-handled errors keep an application running, tell the user something useful and give developers the detail they need. Poorly handled errors produce blank screens, silent data loss and bug reports that are impossible to reproduce.

The Error object

JavaScript has a built-in Error type, and every thrown error is usually an instance of it.

// error.js
const error = new Error("Something broke");

error.message; // "Something broke"
error.name;    // "Error"
error.stack;   // the call stack at the point of creation
error.cause;   // an optional underlying error

There are several built-in subclasses for common categories: TypeError, ReferenceError, RangeError, SyntaxError, URIError and EvalError. They mostly differ in name and the situations that produce them, which makes them useful for debugging and for branching on the kind of failure.

Throwing errors

Use throw to signal that a rule has been violated. Throw an Error object, never a string or a number — only Error instances carry a message, a name and a stack trace.

// validate.js
function divide(a, b) {
  if (b === 0) {
    throw new Error("Cannot divide by zero");
  }
  return a / b;
}

Throwing early, at the exact point where an assumption breaks, keeps invalid state from spreading deeper into your program. Failing loudly in development is a feature, not a nuisance.

try, catch and finally

try runs risky code. If anything throws, control jumps to catch. finally runs either way, which makes it the place for cleanup.

// try.js
try {
  const data = JSON.parse(input);
  save(data);
} catch (error) {
  console.error("Invalid data:", error.message);
} finally {
  setLoading(false);
}

The catch parameter is the thrown value. Modern JavaScript also supports optional catch binding when you do not need it: catch { ... }. Inside a catch block you can inspect the error, log it, recover, or re-throw it if you cannot handle it.

// rethrow.js
try {
  await loadProfile();
} catch (error) {
  log(error);
  throw error; // let a higher layer decide
}

Custom error classes

Generic errors lose meaning once your app grows. Custom classes let you attach context and branch on the type of failure.

// http-error.js
class HttpError extends Error {
  constructor(status, message) {
    super(message ?? `HTTP ${status}`);
    this.name = "HttpError";
    this.status = status;
  }
}

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

Now callers can react precisely:

// handle.js
try {
  await save(form);
} catch (error) {
  if (error instanceof ValidationError) {
    showFieldError(error.field, error.message);
  } else if (error instanceof HttpError && error.status === 401) {
    redirectToLogin();
  } else {
    showGenericError();
  }
}

This is the difference between “something went wrong” and “the email field is already taken”.

Wrapping errors with cause

Sometimes you want to add context without losing the original failure. The cause option preserves the chain.

// cause.js
try {
  await db.query(sql);
} catch (error) {
  throw new Error("Failed to load orders", { cause: error });
}

The outer error carries the user-friendly context, while error.cause keeps the original detail for logs and debugging.

Errors in asynchronous code

Async code has two extra failure paths. With async/await, try/catch works exactly as it does for synchronous code.

// async.js
async function load() {
  try {
    const res = await fetch("/api/data");
    if (!res.ok) throw new HttpError(res.status);
    return await res.json();
  } catch (error) {
    console.error("Load failed:", error);
    throw error;
  }
}

Without await, attach a handler to the promise:

// promise.js
fetch("/api/data")
  .then((res) => res.json())
  .catch((error) => console.error(error));

A rejected promise with no handler becomes an unhandled rejection. In browsers it logs a warning; in Node.js it can terminate the process. Always handle rejections, even if only to log and re-throw.

Global safety nets

Even with careful code, something will slip through. Global handlers catch what you missed so the app can report it instead of dying silently.

// global.js
window.addEventListener("error", (event) => {
  reportToServer(event.error);
});

window.addEventListener("unhandledrejection", (event) => {
  reportToServer(event.reason);
});

In Node.js the equivalents are process.on("uncaughtException") and process.on("unhandledRejection"). Treat these as last resorts for logging and graceful shutdown, not as a substitute for handling errors where they occur.

Validate before you throw

Not every problem is exceptional. Expected conditions — empty input, a missing optional field, a search with no results — are better handled with validation than exceptions.

// guards.js
function formatName(user) {
  if (!user?.name) return "Anonymous";
  return user.name.trim();
}

Reserve exceptions for genuinely unexpected or unrecoverable situations. Using them for ordinary control flow makes code slower and harder to follow.

Talking to users

A technical error message is for developers. Users need to know what happened and what they can do next.

  • Keep messages short and human: “We could not save your changes. Check your connection and try again.”
  • Avoid jargon, stack traces and raw status codes in the interface.
  • Provide a next step: retry, go back, contact support or continue offline.
  • Log the full detail — message, stack, context — so support can investigate.

Best practices

  • Throw Error objects, never strings.
  • Catch only where you can recover or add useful context.
  • Use custom error classes for domain-specific failures.
  • Wrap errors with cause instead of discarding the original.
  • Always handle promise rejections.
  • Add global handlers as a final safety net, and log to a real service.
  • Validate expected conditions instead of throwing.
  • Keep user messages separate from developer diagnostics.
  • Write tests for your failure paths, not just the happy path.

Common mistakes

  • Swallowing errors with an empty catch block.
  • Throwing strings and losing the stack trace.
  • Catching an error and returning a default that hides a real bug.
  • Forgetting to handle rejected promises.
  • Showing raw error messages to users.
  • Using exceptions for ordinary control flow.
  • Assuming try/catch around a promise executor catches asynchronous errors — it does not.

Where to go next

Errors are where async code and the Fetch API meet reality. Pair the patterns here with the DOM to render friendly states, and revisit JavaScript fundamentals whenever a TypeError reminds you that undefined is not a function.

Throwing errors

Always throw Error objects. They carry a stack trace and a message; strings carry neither.

Prefer
if (!user) {
  throw new Error("User not found");
}
Avoid
if (!user) {
  throw "User not found";
}

Expected vs exceptional

Validate expected conditions; reserve try/catch for things you cannot predict.

Prefer
if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}
Avoid
try {
  JSON.parse(input);
} catch {
  // swallowing every error hides bugs
}

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Error Handling?

Our interactive tutorial walks you through Error Handling step by step — with quizzes and real code you can run in the browser.