Skip to content
JavaScript js error-handling 4 min read

Throwing Errors

When something goes wrong in your code — invalid input, a missing resource, a broken invariant — you need a way to stop normal execution and signal that failure to whoever called you. JavaScript does this with the throw statement, which raises an exception that travels up the call stack until something catches it. Throwing well-formed errors with clear messages is one of the highest-leverage habits you can build: it turns cryptic, hard-to-debug failures into precise, actionable signals.

The throw statement

throw interrupts the current function immediately. Any expression that follows it becomes the thrown value, and control jumps to the nearest enclosing catch block (or terminates the program if there is none).

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

divide(10, 0);

Output:

Uncaught Error: Cannot divide by zero
    at divide (<anonymous>:3:11)
    at <anonymous>:7:1

Everything after a reached throw in the same scope is unreachable — the function does not return normally.

Throw Error objects, not primitives

You can throw any value — a string, a number, an object, even undefined — but you almost always should not. Throwing an Error object (or a subclass) gives you a name, a human-readable message, and a captured stack trace for free. Primitives carry none of that context.

// Avoid: no stack trace, no type information
throw "Something failed";
throw 404;

// Prefer: rich, debuggable, and works with instanceof
throw new Error("Something failed");

Warning: Code that catches errors often assumes it received an Error and reads err.message or err.stack. Throwing a string or number breaks those assumptions and silently produces undefined properties downstream.

The difference shows up immediately when you inspect what was caught:

Thrown valueerr.messageerr.stackerr instanceof Error
new Error("boom")"boom"full tracetrue
"boom"undefinedundefinedfalse
404undefinedundefinedfalse

The Error object

The built-in Error constructor accepts a message string and an optional options object. The instance it produces exposes a small but important set of properties.

PropertyDescription
messageThe human-readable string passed to the constructor.
nameThe error category, e.g. "Error" or "TypeError". Used in toString().
stackA non-standard but universally supported string describing where the error was created.
causeThe underlying error or value that triggered this one (ES2022, options bag).
const err = new Error("Database connection failed");

console.log(err.name);
console.log(err.message);
console.log(err.toString());

Output:

Error
Database connection failed
Error: Database connection failed

Built-in subclasses like TypeError, RangeError, and SyntaxError set name automatically, which makes your throw statements self-describing:

function setVolume(level) {
  if (typeof level !== "number") {
    throw new TypeError(`Expected a number, received ${typeof level}`);
  }
  if (level < 0 || level > 100) {
    throw new RangeError("Volume must be between 0 and 100");
  }
  return level;
}

Error cause

The cause option (ES2022) lets you attach the original error when you wrap one failure in another. This preserves the full chain instead of discarding low-level detail.

async function loadProfile(userId) {
  try {
    return await fetch(`/api/users/${userId}`).then((r) => r.json());
  } catch (networkError) {
    throw new Error(`Failed to load profile for ${userId}`, {
      cause: networkError,
    });
  }
}

The high-level message stays readable, while err.cause retains the original network error for logging and debugging.

Writing helpful messages

A good error message states what went wrong and, ideally, gives enough context to fix it. Include the offending value, but never embed secrets such as passwords or tokens.

// Vague — forces a debugging session
throw new Error("Invalid input");

// Specific — diagnosable at a glance
throw new Error(`Invalid email "${email}": missing "@" symbol`);

Rethrowing

Inside a catch block you frequently inspect an error, decide you cannot handle it, and pass it along. Rethrowing the same object preserves the original stack trace; creating a new one (without cause) loses it.

function parseConfig(raw) {
  try {
    return JSON.parse(raw);
  } catch (err) {
    if (err instanceof SyntaxError) {
      // Handle: this layer knows how to recover
      return {};
    }
    throw err; // Anything else is not ours to handle — pass it up
  }
}

When you want to add context while still preserving the original, rethrow a new error with cause rather than swallowing the old one.

Best Practices

  • Always throw Error instances (or subclasses), never strings, numbers, or plain objects.
  • Pick the most specific built-in type — TypeError, RangeError — so callers can branch on instanceof.
  • Write messages that name the failure and include the relevant value, but never leak secrets.
  • Use the cause option to wrap low-level errors without losing their stack trace.
  • Rethrow the original error object (throw err) when you only inspected it; don’t recreate it and discard the stack.
  • Never throw inside a finally block — it overrides any error already in flight and hides the real cause.
  • Fail fast: validate inputs and throw at the top of a function instead of letting bad data propagate.
Last updated June 1, 2026
Was this helpful?