skip to content

throw Semantics and Throwing Non-Errors

In JavaScript you can throw literally any value — a string, an object, even undefined — and that flexibility is a trap for callers who expect .message and .stack. Interviewers ask this to check that you always throw Error instances and know how rethrowing preserves or destroys context.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

4

In JavaScript, which values are legal after `throw`, and what concretely breaks in the catching code when a function throws the string 'Network unavailable' instead of `new Error('Network unavailable')`?

level: juniorimportance: must knowfreq 72%

answer

  1. any value is throwable
  2. no type check on the expression
  3. only Errors carry name, message, stack
  4. err.message on a string is undefined
  5. throw undefined breaks the catch block itself

basics

~20 s

JavaScript's throw accepts any value: strings, numbers, plain objects, null, even undefined. Only Error instances carry name, message and a stack, so a thrown string leaves the catch block with no diagnostics, err.message undefined, and err instanceof Error false.

solid answer

~50 s

`throw` evaluates its expression and throws whatever that produces — there is no type check, so `throw 'boom'`, `throw 42`, `throw { code: 404 }` and `throw undefined` are all valid and all reach a `catch` clause unchanged. The damage lands on the catcher. Error instances are the only values that come with a `name`, a `message` and (in every mainstream engine) a `stack` captured at construction, so a thrown string gives `err.message === undefined`, `err.stack === undefined`, and `err instanceof Error === false`, which quietly breaks every guard written that way. `throw undefined` is worse: the catch block's own `err.message` read is itself a TypeError. Uncaught, a non-Error prints as a bare value with no call site, so logs and crash reporters have nothing to group on. The rule is: always throw an Error (or an Error subclass), and put structured data on properties of it.

code

javascript · 26 lines
javascript
function loadConfig() {
  throw 'Network unavailable';
}

try {
  loadConfig();
} catch (err) {
  console.log(typeof err);            // "string"
  console.log(err.message);           // undefined
  console.log(err.stack);             // undefined
  console.log(err instanceof Error);  // false
}

try {
  throw undefined;
} catch (err) {
  try {
    console.log(err.message);
  } catch (secondary) {
    console.log(secondary.name);      // "TypeError"
  }
}

const good = new Error('Network unavailable');
good.code = 'E_NETWORK';
console.log(good.name, good.message, typeof good.stack); // "Error" "Network unavailable" "string"

go deeper

for a junior

Be able to say plainly that any value can be thrown, that only Error instances have message, name and stack, and that you should always throw new Error(...). Knowing err.message is undefined for a thrown string is enough here.

for a middle

Explain the mechanics: the stack is captured when the Error is constructed, instanceof Error guards silently miss non-Errors, and throw undefined turns the catch block's own property read into a TypeError. Show how to attach a code property instead of throwing data.

for a senior

Demonstrate that you handle the consumption side too: your catch blocks call third-party and user code, so they must survive receiving anything. Talk about what uncaught non-Errors do to log grouping and crash reporting, and how you enforce the produce-only-Errors rule in review or lint.

for a principal

Own the convention across a codebase: one error shape at every boundary, structured fields on the Error rather than bespoke throwables, and a normalization point where foreign values become Errors so downstream logging, alerting and dedup keep working.

## throw does not type-check A `throw` statement has the form `throw <expression>;`. The engine evaluates the expression and throws the resulting **value** — the specification puts no constraint on its type. All of these are legal JavaScript and all of them are caught by an ordinary `catch` clause: ```js throw 'boom'; throw 42; throw { code: 'E_TIMEOUT', retryable: true }; throw null; throw undefined; throw Symbol('nope'); ``` The catch binding receives the value exactly as it was thrown. There is no wrapping, no boxing, no coercion to an Error. This is unlike languages that restrict throwing to a throwable base type, and it is the whole reason the topic exists in interviews. ## What an Error instance gives you that a raw value does not `new Error('Network unavailable')` produces an object with: - **`message`** — the text you passed; - **`name`** — `'Error'`, or the subclass name for `TypeError`, `RangeError`, and friends; - **`toString()`** — `'Error: Network unavailable'`, which is what string conversion and most loggers show; - **`stack`** — a non-standard but universally implemented string containing the call site, captured **at construction time**, not at throw time. None of that exists on a string, a number, or a plain object literal. A plain object can of course *have* a `message` property, but it still has no stack, and nothing in it tells a generic error handler that it represents a failure. ## The four things that break in the caller ```js try { loadConfig(); // throws 'Network unavailable' } catch (err) { console.log(err.message); // undefined -> log line says "undefined" console.log(err.stack); // undefined -> no call site anywhere console.log(err instanceof Error); // false -> every type guard misses it console.log(err.name); // undefined -> name-based dispatch misses it } ``` 1. **Diagnostics vanish.** The single most useful artefact of a failure is the stack. A thrown string has none, and no engine will manufacture one for you afterwards. 2. **Type guards silently fail.** Code that branches on `err instanceof Error` — including error-handling middleware, test helpers and reporting SDKs — takes the "this is not an error" path. 3. **Property reads misbehave or crash.** On a string, `err.message` is `undefined` and you log the word "undefined". On `throw undefined` or `throw null`, the read `err.message` throws a **TypeError inside your catch block**, replacing the original failure with a confusing second one. 4. **String conversion is not safe either.** `String(err)` looks like a defensive fallback, but if the thrown value is an object with a null prototype (`Object.create(null)`) or a `toString` that itself throws, the conversion throws too. ## Uncaught non-Errors When nothing catches it, the host reports the thrown value. An uncaught `new Error('boom')` prints the message plus the stack, which is what makes the report actionable. An uncaught `'boom'` prints the bare value with no call site — Node.js even suggests running with `--trace-uncaught` because it cannot tell you where the throw happened from the value alone. Crash-reporting tools group and deduplicate on the stack, so non-Error throws tend to land in one useless bucket. ## Where non-Error throws actually come from Almost nobody writes `throw 'boom'` deliberately today, yet catch blocks still receive junk, for three reasons: - old or hand-rolled layers that threw `{ status: 400, body }` as a control-flow signal; - third-party or user-supplied code you call — you do not control what *it* throws; - values that are thrown implicitly by constructs you did not write, which may still be anything. That is why the discipline has two halves: **produce** only Errors, and **consume** defensively. ## The rule, and how to carry data Always throw an `Error` (or a subclass of it). When you need machine-readable detail, attach it as a property rather than replacing the Error with a data object: ```js const err = new Error('Network unavailable'); err.code = 'E_NETWORK'; err.retryable = true; throw err; ``` The caller keeps `message`, `name`, `stack` and `instanceof Error`, and still gets `err.code` for branching. You lose nothing and gain everything a debugger, a log aggregator and a type guard need.

  • If you need to send a machine-readable error code to the caller, why not just throw the data object with that code on it?
    Because you would trade every diagnostic for one property. Create the Error and attach the field to it: `const e = new Error('...'); e.code = 'E_NETWORK'; throw e;`. The caller can still branch on `e.code` while keeping `message`, `name`, `stack` and a working `instanceof Error` check. Structured detail and Error-ness are not mutually exclusive.
  • Is `throw new Error(someCaughtValue)` a reasonable way to convert an unknown value into an Error?
    It works but it is lossy and often ugly. The Error constructor converts its first argument to a string, so `new Error(originalError)` produces the message `'Error: original message'`, and `new Error({code: 404})` produces `'[object Object]'`. It also throws if the value cannot be converted to a primitive. Convert deliberately, and keep the original value attached rather than flattening it into text.
  • Does the engine capture the stack when the value is thrown, or when the Error is created?
    At creation. `new Error()` captures the stack at the point of construction in every mainstream engine, so an Error created in one place and thrown much later points at the construction site, not the throw site. That is also why a non-Error can never acquire a stack: there was no Error construction to capture one.

saying these in an interview costs you the question

  • Claims JavaScript only allows Error objects to be thrown
  • Says throwing a string is fine because catch still runs
  • Assumes err.message always exists inside a catch block
  • Thinks the engine attaches a stack to whatever value is thrown
  • Throws plain data objects as a normal control-flow signal

context

open as a page

Inside a JavaScript catch block you decide the failure is not yours to handle. Compare `throw err;`, `throw new Error(err.message);`, and logging the error and returning — what does each leave the outer caller with?

level: middleimportance: must knowfreq 56%

basics

~20 s

A bare throw err rethrows the identical object, so its type, custom properties and original stack all survive. throw new Error(err.message) starts a fresh error and discards them. Logging and returning is worst: the caller sees a successful call and a bogus value.

open as a page

A JavaScript library invokes user-supplied callbacks and third-party code, so its catch blocks can receive absolutely any value. How do you turn an unknown caught value into something safe to log and rethrow, without the error path itself throwing?

level: seniorimportance: should knowfreq 41%

basics

~20 s

Normalize at the boundary: check instanceof Error, fall back to Object.prototype.toString branding for cross-realm errors, and otherwise construct an Error from a defensively stringified value. Stringification must sit inside its own try, because null-prototype objects and throwing toString methods make String() throw.

open as a page

In JavaScript, why is putting the thrown value on the line after `throw` a syntax error, and how do you throw from an expression position such as an arrow function's concise body or the right-hand side of `??`?

level: middleimportance: nice to knowfreq 27%

basics

~20 s

The grammar forbids a line terminator between throw and its expression, and there is no valid argument-less throw, so a newline is a hard syntax error rather than silent semicolon insertion. throw is a statement, not an expression, so expression positions need a helper function or an immediately-invoked arrow.

open as a page