Throwing Errors in JavaScript: Complete Guide
The throw statement stops execution and signals a problem. Learn what to throw, when to throw, how to avoid the ASI pitfall, and the right patterns for rethrowing errors.
The throw statement stops the current function immediately and signals that something went wrong. The thrown value travels up the call stack until a try...catch block handles it. If nothing catches it, the script terminates.
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
console.log(divide(10, 2)); // 5Calling divide(10, 0) instead throws immediately: no code after the throw line runs, and the error travels upward through every function that called divide until something catches it or the program crashes.
What to throw
You can throw any JavaScript value, but you should always throw an Error object so the catching code gets a message, a name, and a stack trace.
throw "Something went wrong"; // Bad, no stack trace
throw 404; // Bad, completely unhelpful
throw new Error("User not found"); // GoodA string or a number thrown on its own carries no information about where it came from. An Error object gives you three things a raw value does not: a message describing what happened, a name identifying the error type, and a stack trace showing exactly where it originated.
You can go one step further and throw a specific error type:
throw new TypeError("Expected a string, got number");
throw new RangeError("Value must be between 1 and 100");Choosing TypeError or RangeError over a plain Error lets a catch block branch on error.name or use instanceof, without parsing the message text. You can also throw custom error types for even more specific handling:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validateEmail(email) {
if (!email.includes("@")) {
throw new ValidationError("Email must contain @", "email");
}
}The field property here is something a plain Error could never carry, which is why a dedicated class earns its keep for validation failures. For more on custom errors, see the custom errors guide.
The ASI pitfall: never break before the expression
Automatic Semicolon Insertion, or ASI, is a JavaScript feature that inserts semicolons at line breaks where they seem needed. This creates a dangerous trap with throw, because a line break right after the keyword silently breaks the statement:
throw
new Error("Something failed");ASI transforms this into throw; followed by a separate, unreachable expression statement. throw on its own is invalid syntax, so this throws a SyntaxError instead of the error you meant to create.
Always keep the expression on the same line as throw. If the expression is long, wrap it in parentheses instead of breaking the line:
throw new Error(
`Failed to process user ${userId}: ${details}`
);The opening parenthesis of the Error constructor call is what keeps this safe: the line break happens inside the argument list, not immediately after the throw keyword, so ASI has nothing to insert a semicolon into.
When to throw
Throw when a function cannot do its job. The most common trigger is invalid input, checked at the top of the function before any real work happens:
function getUser(id) {
if (typeof id !== "number" || id <= 0) {
throw new TypeError("User ID must be a positive number");
}
return database.query("SELECT * FROM users WHERE id = ?", [id]);
}This is called fail fast: throwing at the start of a function, right after validating inputs, prevents bad data from traveling deeper into the application where the cause becomes harder to trace.
Throw for unrecoverable situations, not for expected outcomes. If a search function finds no results, return an empty array or null. If a required config value is missing, throw.
| Return | Throw |
|---|---|
| Expected outcome | Unexpected problem |
| Caller can continue | Caller must decide how to recover |
| Example: search returns an empty list | Example: required API key is missing |
Rethrowing errors
Catch an error, do something useful with it such as logging, then throw it again to let a higher-level handler decide what to do:
async function saveUserPreferences(userId, prefs) {
try {
await api.post(`/users/${userId}/preferences`, prefs);
} catch (error) {
console.error("Failed to save preferences:", error);
throw error; // Logged here, but the caller still needs to know
}
}A caller further up the chain can now show its own message without needing to know anything about the API layer underneath, since the rethrown error already reached it through the normal catch mechanism:
try {
await saveUserPreferences(1, { theme: "dark" });
} catch (error) {
showToast("Could not save your preferences. Please try again.");
}You can also wrap the original error with more context instead of rethrowing it unchanged. Pass the original as the cause option so the low-level failure stays attached to the new, higher-level message:
try {
const config = JSON.parse(rawConfig);
} catch (error) {
throw new Error("Failed to parse config file", { cause: error });
}The cause option preserves the original error for debugging while adding a higher-level message for the caller, so nothing about the original JSON failure gets lost in translation.
Throwing in async code
Throwing inside an async function automatically rejects the returned promise, so callers can use either await with try...catch or .catch on the promise chain:
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: User not found`);
}
return response.json();
}The thrown error becomes a promise rejection, so a caller that never uses await can still catch it on the promise chain instead:
fetchUser(999)
.then((user) => console.log(user))
.catch((err) => console.error(err.message));However, throwing in a plain callback does not propagate to the promise or outer scope, because the callback runs later on its own turn of the event loop, after the surrounding try block has already finished:
setTimeout(() => {
try {
throw new Error("Timer error");
} catch (e) {
console.log("Caught:", e.message);
}
}, 1000);Wrapping the setTimeout call itself in try...catch, instead of wrapping the callback body as shown above, would never catch the timer error at all.
Common mistakes
Throwing strings is the most common mistake. A thrown string has no stack trace, so when it surfaces in logs, there is no way to tell where it came from. Always use throw new Error(...) instead.
Throwing in constructors silently is a quieter trap. If a constructor throws, new returns nothing and the variable stays undefined. This is correct behavior, but it can surprise you if you are not expecting it:
class User {
constructor(name) {
if (!name) throw new Error("Name is required");
this.name = name;
}
}
const u = new User(""); // Throws, u is never assignedUsing throw for control flow is the third mistake. Do not use throw and catch as a substitute for return or break, since exceptions are meant for exceptional situations, not normal branching:
function findItem(items, id) {
try {
items.forEach((item) => {
if (item.id === id) throw item; // Misusing throw as a return
});
} catch (found) {
return found;
}
return null;
}The built-in array method find already solves the same problem without any exception handling at all, and the resulting code reads far more clearly to the next person who touches it:
function findItem(items, id) {
return items.find((item) => item.id === id) ?? null;
}For the catching side of error handling, see the try...catch guide.
Rune AI
Key Insights
- Use throw new Error('message') to stop execution and signal a problem.
- Always throw Error objects, never plain strings or numbers.
- Throw early: validate inputs at the top of a function and throw before anything bad happens.
- Never put a line break between throw and the expression; ASI will break the statement.
- Rethrow in catch blocks when you want to log an error and then let a higher-level handler deal with it.
- Throw for unrecoverable problems; return for expected outcomes.
Frequently Asked Questions
Should I always throw Error objects?
What happens if no catch block handles my throw?
Can I throw inside a catch block?
Conclusion
The throw statement is how your code says this should not happen and I cannot continue. Always throw Error objects so callers get stack traces and meaningful messages. Throw early in your functions when inputs are invalid, and rethrow when you catch an error you cannot fully handle. The key distinction is that throw signals a failure the caller must deal with, while return signals a result the caller asked for.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.