Errors & Exceptions

Intermediate · 18 min read · ▶ live playground · ✦ checkpoint

Bad data isn't a rare disaster; it's Tuesday. An exception is a signal that the normal path can't continue safely, and JavaScript gives you throw, try, catch, and finally to handle that signal without turning the rest of your program into guesswork.

Throw an honest error

The keyword throw means "stop this normal path and send this value outward." Think of it like pulling a fire alarm: you do it when the room is unsafe, not when you merely want to choose a different hallway.

In real JavaScript, throw an Error object, which is a value with a name, a message for human-readable text, and debugging details.

function awardBonus(score) {
  if (score < 0) {
    throw new RangeError("Score cannot be negative");
  }
 
  return score + 10;
}
 
console.log(awardBonus(18));
console.log(awardBonus(-4));
console.log("saved");

28 prints, then execution stops with RangeError: Score cannot be negative. saved never prints.

That is the point. A negative score is not a normal score. Returning 0, null, or "bad" would make every caller guess what happened. Throwing says the current path has no honest result.

Catch what you can handle

A try block is code you allow to fail, a catch block is code that runs when that failure is thrown, and a finally block is code that runs at the end either way. Picture a workshop: try uses the sharp tool, catch handles the cut, and finally puts the tool away.

function parseScore(rawScore) {
  const score = Number(rawScore);
 
  if (Number.isNaN(score)) {
    throw new TypeError(`Score must be a number: ${rawScore}`);
  }
 
  if (score < 0) {
    throw new RangeError("Score cannot be below 0");
  }
 
  return score;
}
 
const rawScores = ["42", "not ready", "-1"];
 
for (const rawScore of rawScores) {
  try {
    const score = parseScore(rawScore);
    console.log(`${rawScore} -> ${score + 5}`);
  } catch (error) {
    console.log(`${rawScore} failed: ${error.message}`);
  } finally {
    console.log(`checked: ${rawScore}`);
  }
}

42 -> 47
checked: 42
not ready failed: Score must be a number: not ready
checked: not ready
-1 failed: Score cannot be below 0
checked: -1

Notice the shape: the loop keeps going because this code knows how to handle one bad score and move to the next one. If the catch block cannot do something useful, do not catch the error there. Let it keep traveling to code that has more context.

Read the error object

A built-in error type is a ready-made Error family JavaScript already provides. You have already seen ReferenceError, SyntaxError, and TypeError; RangeError is another common one for values outside an allowed range.

Error objects normally carry name and message. Many engines also expose a stack, which is debugging text that trails the function calls that led to the error, like breadcrumbs back through the program. The wording and availability are engine-specific. A cause is present when you attach the original error with the { cause: originalError } option, like stapling the original receipt to the new report.

const original = new TypeError("Score must be a number");
const wrapped = new Error("Could not save leaderboard", { cause: original });
 
console.log(original.name);
console.log(original.message);
console.log(wrapped.name);
console.log(wrapped.cause === original);
console.log(wrapped.cause.message);
console.log(typeof wrapped.stack);

TypeError, Score must be a number, Error, true, Score must be a number, then string.

stack text varies by engine, so read it for function names and locations rather than exact wording. cause helps when you catch a low-level problem and throw a clearer higher-level one.

Write a custom error class

A custom error class is your own Error family for one kind of problem. A class is a blueprint for making objects; the full class story comes in Section 11, but this small pattern is common enough to read now.

The keyword extends means "make this class a specialized version of that class." The call super(...) runs the parent Error setup before your class adds its own fields. A constructor is the setup function that runs when new creates the object.

class MissingFieldError extends Error {
  constructor(fieldName, options = {}) {
    super(`Missing required field: ${fieldName}`, options);
    this.name = "MissingFieldError";
    this.fieldName = fieldName;
  }
}
 
function formatPlayer(record) {
  if (record.name === undefined) {
    throw new MissingFieldError("name", {
      cause: new Error(`Record ${record.id} had no name`),
    });
  }
 
  return `${record.name} scored ${record.score}`;
}
 
try {
  console.log(formatPlayer({ id: "p2", score: 12 }));
} catch (error) {
  console.log(error.name);
  console.log(error.message);
  console.log(error.fieldName);
  console.log(error.cause.message);
}

MissingFieldError, Missing required field: name, name, then Record p2 had no name.

Custom errors pay off when different failures need different responses. A missing field, a bad range, and broken JSON are not the same problem, so your error names should not flatten them into one vague "Error happened".

Try it — rescue one record at a time

Run this, then inspect accepted and rejected at the prompt. Change the bad records and run again.

javascript — live consolereal engine
⌘/Ctrl + Enter to run

The program does not pretend every record is fine. It accepts the records it can honestly format, records the failures it understands, and leaves clear messages for the data you need to fix.

Choose the right response

When an error reaches you, pick one of three moves:

  • Handle it: show a clear message, skip one bad record, or use a safe fallback you trust.
  • Wrap it: throw a clearer error with cause so the next layer sees the user-level problem and the original detail.
  • Let it pass: do nothing here when this part of the code cannot respond well.

Do not hide errors just to keep the console quiet. A swallowed error is like taking the batteries out of a smoke alarm because the sound is annoying. The program feels calmer right before it becomes harder to debug.

This lesson stayed with code that runs now, in order. Callbacks that run later get extra error-handling rules in 12.3; next, you will make fewer exceptions happen by validating data as it enters your program in 9.2.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion