JavaScriptthrow Statement

The throw Statement

throw is JavaScript's way to signal that something is wrong. It immediately stops the current execution path and starts looking for a matching catch block up the call stack — unwinding stack frames, running finally blocks along the way, until something handles it or the runtime gives up. The expression you throw can be any value, but in practice you should always throw an Error.

The syntax

throw takes a single expression. It cannot take an empty expression, and it does not accept a line break between throw and the value (a long-standing automatic-semicolon-insertion footgun).

JS
throw new Error("invalid input");

// Don't do this — ASI inserts a semicolon after `throw` and you get a SyntaxError:
// throw
//   new Error("oops");
You can throw any value

The language does not restrict what you can throw. Strings, numbers, objects, even undefined — they all propagate the same way. The catch block receives whatever was thrown.

JS
try {
  throw "bad input";
} catch (err) {
  console.log(typeof err, err);   // "string" "bad input"
}

try {
  throw { code: 404, message: "not found" };
} catch (err) {
  console.log(err.code);          // 404
}
But always throw Error instances
Strings have no stack trace, plain objects do not respond to `instanceof Error`, and error trackers expect a real `Error`. Even when you have only a message, `throw new Error("...")` is the right call. The cost is zero, the gain is a captured stack and a recognisable type.
Creating Error instances

new Error(message) is the baseline. You can also pass { cause } as a second argument to chain a lower-level error, and the built-in subtypes (TypeError, RangeError) accept the same shape.

JS
function parseConfig(text) {
  try {
    return JSON.parse(text);
  } catch (cause) {
    throw new Error("config is not valid JSON", { cause });
  }
}

try {
  parseConfig("not-json");
} catch (err) {
  console.log(err.message);     // "config is not valid JSON"
  console.log(err.cause);       // the original SyntaxError
}
Pick the right subtype
Use `TypeError` for "argument is the wrong type", `RangeError` for "argument is out of bounds", and a custom subclass for domain errors. The closer the type matches the failure, the easier it is to handle correctly upstream.
throw inside conditions

Guard clauses are one of the most common shapes of throw. They keep the happy path flat and put failure cases up top.

JS
function withdraw(account, amount) {
  if (typeof amount !== "number" || Number.isNaN(amount)) {
    throw new TypeError("amount must be a number");
  }
  if (amount <= 0) {
    throw new RangeError("amount must be positive");
  }
  if (amount > account.balance) {
    throw new Error("insufficient funds");
  }
  account.balance -= amount;
  return account.balance;
}
throw is an expression-position killer

Because throw is a statement, you cannot use it in expression positions — not in a || throw, not as a default arrow body, not directly inside a ternary. A common workaround is a small helper:

JS
// Helper that "throws as an expression"
const fail = (msg) => { throw new Error(msg); };

const user = users.find((u) => u.id === id) ?? fail("user not found");
const port = Number(process.env.PORT) || fail("PORT is required");

A TC39 proposal to make throw usable as an expression has been discussed for years, but is not in the language yet. The helper above is the idiomatic stand-in.

Re-throwing

A catch block can inspect an error and either handle it or re-throw it. Re-throwing keeps the original stack trace, so the upstream caller still sees where it came from.

JS
try {
  await loadData();
} catch (err) {
  if (err instanceof NetworkError) {
    showOfflineBanner();
    return;
  }
  throw err;        // anything else propagates with its original stack
}

If you want to add context, throw a new error with the original as cause — that keeps both stacks linked.

JS
try {
  await readFile(path);
} catch (cause) {
  throw new Error(`failed to read ${path}`, { cause });
}
throw is fast — but not free

Capturing a stack trace involves walking the call stack and stringifying frames. For exceptional cases that is negligible. For "expected" failure modes inside a hot loop, throwing/catching can slow code down compared to a result-style return.

  • Use throw for exceptional paths — programmer errors, invariants, conditions you do not normally expect.

  • Use return values (or a [error, value] tuple) for expected failures inside hot paths — validation, lookups that often miss.

  • Never use throw for control flow inside tight loops. It works, but it is slower and harder to read than a return.

throw inside async functions

Throwing inside an async function does not produce a synchronous exception at the call site — it rejects the promise the function returns. From the caller's point of view there is no difference between throw new Error(...) and return Promise.reject(new Error(...)).

JS
async function risky() {
  throw new Error("nope");
}

risky().catch((err) => console.log(err.message));   // "nope"
The 30-second rule
When you write `throw`, ask: *will the next developer understand from the message alone what went wrong and what to fix?* If not, add context — include the offending value, attach a `cause`, or use a domain-specific subclass. Future-you will appreciate it.