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.

6 min read

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.

javascriptjavascript
function divide(a, b) {
  if (b === 0) {
    throw new Error("Cannot divide by zero");
  }
  return a / b;
}
 
console.log(divide(10, 2)); // 5

Calling 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.

javascriptjavascript
throw "Something went wrong"; // Bad, no stack trace
throw 404;                    // Bad, completely unhelpful
throw new Error("User not found"); // Good

A 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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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.

ReturnThrow
Expected outcomeUnexpected problem
Caller can continueCaller must decide how to recover
Example: search returns an empty listExample: 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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
class User {
  constructor(name) {
    if (!name) throw new Error("Name is required");
    this.name = name;
  }
}
 
const u = new User(""); // Throws, u is never assigned

Using 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:

javascriptjavascript
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:

javascriptjavascript
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

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.
RunePowered by Rune AI

Frequently Asked Questions

Should I always throw Error objects?

Yes. Always throw an Error instance or a subclass like TypeError. Plain strings or numbers do not have stack traces, which makes debugging much harder. Code that catches your errors expects message and stack properties to exist.

What happens if no catch block handles my throw?

The error propagates up the call stack. If no catch block in any calling function handles it, the script terminates and the browser or Node.js prints the error and stack trace to the console.

Can I throw inside a catch block?

Yes, this is called rethrowing. It lets you handle an error partially (like logging it) and then pass it up to a higher-level handler for final resolution.

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.