JAVASCRIPT / ERRORS, STRICT MODE, AND DEBUGGING
Throwing errors with useful messages
Write throws that produce a single log line naming the operation, the rule that was broken, and the offending value, using the right built-in Error type.
What you will learn
- Throw Error objects, not strings, so name, message, and stack survive the catch.
- Name the operation, the requirement, and the actual value in one message.
- Pick TypeError, RangeError, or Error to match the kind of failure you found.
- Describe huge or secret values by type, length, and prefix instead of dumping them.
Understanding Throwing errors with useful messages
`throw` accepts any value, but only an Error object carries a `name`, a `message`, and a stack snapshot taken where it was constructed. Whatever you pass becomes the value the nearest `catch` receives, and in most real systems that catch does one thing: it writes `err.message` into a log line, a toast, or a bug report. By the time a human reads that line, the variables, the request, and the loop index are gone. So write the message for that reader: one sentence, no surrounding context available.
A message earns its keep by answering three questions: which operation refused, what it required, and what it actually got. "Invalid input" answers none of them, while `setVolume: level must be between 0 and 100, got 120` answers all three and needs no follow-up question. The constructor adds a fourth piece for free: `TypeError` means the value was the wrong kind, `RangeError` means it was the right kind but outside the allowed span, and plain `Error` covers everything else. Since the name is printed in front of the message, do not repeat the category word inside the text.
Because `throw` unwinds the function immediately, nothing after it runs, which makes the top of a function the cheapest place to check: the argument is still exactly as the caller passed it, so you can quote it verbatim. Interpolate deliberately, though, since `${value}` turns `{ id: 1 }` into `[object Object]` and makes `"5"` look identical to `5` — the precise confusion the message existed to remove. Use `typeof` plus `JSON.stringify` for small values, and type-with-length for huge or sensitive ones. Keep the fixed words of the message stable so that grepping the codebase for the sentence lands on the exact `throw`.
One message per failed rule beats one message covering several rules, because each `throw` can then state the specific bound it checked instead of a vague summary of all of them.
function describe(value) {
return typeof value === "string" ? JSON.stringify(value) : String(value);
}
function setVolume(level) {
if (typeof level !== "number" || Number.isNaN(level)) {
throw new TypeError(
`setVolume: level must be a number, got ${typeof level} ${describe(level)}`
);
}
if (level < 0 || level > 100) {
throw new RangeError(`setVolume: level must be between 0 and 100, got ${level}`);
}
return level;
}
for (const input of ["80", NaN, 120, 80]) {
try {
console.log("ok:", setVolume(input));
} catch (err) {
console.log(`${err.name}: ${err.message}`);
}
}An error message is read alone, with no access to the program's state, so it must name the operation, the rule it enforced, and the value that broke it.
Worked examples
Throwing a string throws away the details
Shows what a catch block actually holds when the thrown value is not an Error.
function parseAge(text) {
const years = Number(text);
if (!Number.isInteger(years)) throw "bad age";
return years;
}
try {
parseAge("twelve");
} catch (err) {
console.log(typeof err);
console.log(err instanceof Error);
console.log(err.message);
console.log(err.stack);
}Example explained
Line 1`typeof err` is "string" because the catch receives the thrown value untouched; nothing wraps it in an Error for you.
Line 2`err instanceof Error` is false, so any handler that inspects the error's type ignores this failure.
Line 3`err.message` is undefined since a string primitive has no `message` property, so the usual `console.log(err.message)` logs nothing useful and "bad age" is lost.
Line 4`err.stack` is undefined, so there is no record of where the throw happened.
Say which item failed
Compares a generic message with one that identifies the exact record in a loop.
const rows = [
{ id: "u1", email: "ada@example.com" },
{ id: "u2", email: "" },
];
function vagueCheck(row) {
if (!row.email) throw new Error("Invalid input");
}
function usefulCheck(row, index) {
if (!row.email) {
throw new Error(`rows[${index}] (id=${row.id}): email must not be empty`);
}
}
for (const check of [vagueCheck, usefulCheck]) {
try {
rows.forEach(check);
} catch (err) {
console.log(err.message);
}
}Example explained
Line 1Both functions fail on the same record, so the only difference between the two log lines is how much work they save you.
Line 2"Invalid input" confirms that something broke but not which row, which field, or what the value was, so you have to reproduce the run to find out.
Line 3`usefulCheck` interpolates the loop index and the record's own `id`, which points at one row in the source data.
Line 4`rows.forEach(check)` passes the element and its index to the callback, which is why `index` is in scope to put in the message.
Describe a secret by shape, not content
Identifies which value arrived without printing a credential into the log.
function requireApiKey(key) {
if (typeof key !== "string" || !key.startsWith("sk_")) {
const shown = String(key);
throw new Error(
`requireApiKey: key must be a string starting with "sk_", got ${typeof key} of length ${shown.length} starting "${shown.slice(0, 3)}"`
);
}
return key;
}
try {
requireApiKey("pk_live_9f3a2b7c1d");
} catch (err) {
console.log(err.message);
}Example explained
Line 1The message states the rule literally, so the caller can fix the call without opening the function.
Line 2The length and the first three characters are enough to recognise which key was passed, while the rest never reaches the log.
Line 3`String(key)` is what makes this safe for any input: calling `.slice` directly on `undefined` would throw a second TypeError and hide the real problem.
Important notes
`new Error("...")` on its own line creates an object and throws nothing; without the `throw` keyword the function keeps running and the failure resurfaces later, far from the check.
`message` is plain text for humans and the engine never reads it, so do not have callers parse the wording to decide what happened; it breaks the first time someone rephrases the sentence.
Common mistakes
Writing `throw "Something went wrong"`: the catch gets a string, so `err.message` is undefined and `err.stack` does not exist, leaving an empty log line and no throwing location.
Throwing `new Error("Invalid input")` inside a loop over thousands of records: you learn that one record was bad but not which one, so the only way forward is re-running the job with extra logging.
Interpolating values raw, as in `` `got ${value}` ``: an object becomes `[object Object]` and `"5"` is indistinguishable from `5`, so the message hides the very difference that caused the failure.
Try it yourself
Change, predict, then run
In a browser console, write `parsePort(text)` that returns an integer between 1 and 65535 and throws a TypeError or RangeError otherwise, with the function name, the rule, and the received value in each message. Call it with "8080", "eighty", and "70000" in a try/catch that logs only `err.message`, then check that each line alone explains the failure.
Open the JavaScript workspaceCheck your understanding
A service catches every error and stores only `err.message` in its logs. Which throw lets an on-call engineer diagnose the failure from that stored line alone?
- throw new Error("Error in saveUser")
- throw "email must contain @"
- throw new Error(`saveUser: email must contain "@", got "${email}"`)
- throw new TypeError("Invalid email address")
Show answer
Option 3 stands on its own: it names the operation, states the rule, and shows the value that broke it. Option 4 is tempting because TypeError classifies the failure precisely, but only `message` is stored, so the name is dropped and "Invalid email address" still never says which address or which call produced it. Option 2 is worse than it looks, since `err.message` on a thrown string is undefined and the log line ends up empty.