Errors, validation, and defensive code · Lesson 42 of 77 · concept

What an error actually is

About 45 minutes.

By the end of this lesson you can

  • Describe what happens to a running program at the moment something goes wrong.
  • Read any error report and name its two parts: the error's name and its message.
  • Name the four built-in error types you will meet most often, and what each one means.
  • Tell an error apart from a wrong answer that does not stop the program.

An error is not a telling-off

Why it matters

The first time a program stops and prints something angry-looking, it is easy to read it as a verdict on you. It is not. An error is the program reporting, at the earliest moment it could possibly tell, that it has been asked to do something it cannot do. That is the whole of it.

Consider the alternative. A program adding up an order receives the word twelve where it expected the number 12. Without an error it carries on, produces a total of NaN, prints that on an invoice, and emails it to a customer. The error is the version of that story where somebody is told immediately, while the mistake is still cheap.

So the goal of this module is not to make errors go away. It is to put you in charge of them: to decide, for every input your code can receive, whether it should stop loudly, carry on with a stated fallback, or hand the problem back to whoever called it. Those are the only three options, and choosing between them deliberately is what separates code that survives real users from code that does not.

An error is just a value

Explanation

An error in JavaScript is an ordinary object. You can build one with new Error("something went wrong"), put it in a variable, pass it to a function, or print it. Nothing about the object is magical.

Every error carries two pieces of information you will use constantly. Its name says what kind of failure it is: TypeError, RangeError, and so on. Its message is a sentence written for a human. Convert an error to text and JavaScript joins them for you, giving a line like TypeError: volume must be a number.

What makes an error different from other values is not the object at all. It is the verb: throw. Throwing a value leaves the current function immediately, skipping every line after it, and keeps leaving outwards through each caller in turn until something catches the value or the program stops.

The fire alarm, not the fire

An analogy

Think of throw as pulling a fire alarm. Pulling it does not put the fire out, and it does not tidy anything up. What it guarantees is that nobody in the building carries on typing as though nothing were happening.

That is the point. The damage done by a bad value is rarely the bad value itself; it is every later step that trusted it. Throwing stops the trusting.

And, like a fire alarm, it can be answered. Somewhere further out, code can decide that it knows what to do about this and take over. That is lesson three. For now, notice only that between the alarm and the answer, nothing else runs.

Looking at an error without stopping the program

Explanation

Because an uncaught error ends the program, you cannot simply print one and carry on. To look at errors up close we need one piece of syntax slightly early: try and catch.

You write try, then a block of code that might fail, then catch with a name for whatever was thrown, then a block that runs instead. If the try block finishes without incident the catch block is skipped entirely.

Lesson three covers this properly: what it skips, when to use it, and the several ways it is misused. In this lesson it is nothing more than a viewing window, so that the examples below can show you an error instead of merely describing one.

The four you will meet most

Explanation

TypeError means the right question was asked of the wrong kind of thing. Calling something that is not a function, or reading a property of null, both produce one.

RangeError means the right kind of thing, but a value outside what is allowed. Asking a number to print itself with 200 decimal places produces one.

ReferenceError means a name was used that was never declared anywhere.

SyntaxError means the text was not JavaScript at all. It is the odd one out: it is raised while the code is being read, before any of it runs, so a syntax error in a function you never call still stops the file. The same error type is used by JSON.parse when the text it was handed is not JSON, and you will meet that one constantly.

All four are built on Error. For any of them, the check error instanceof Error is true, which is why code can handle every failure the same way when it wants to and separate them by name when it does not.

Errors and wrong answers are different failures

A mental model

Not every failure throws. JavaScript has a long list of operations that answer a nonsense question with a nonsense value instead of an error, and those are the ones that cost you an afternoon.

Number("twelve") gives NaN, a number that is not a number. Reading a property that was never set gives undefined. Asking an array for the position of something it does not contain gives -1. None of these stop anything.

That is what makes them expensive. A thrown error names the line that caused it. A quiet wrong answer flows onward and surfaces three functions later, where the code that reports the problem is nowhere near the code that caused it. A good half of this module is about converting quiet wrong answers into loud errors at the place they happen.

Never write code that depends on an error message

A common mistake

The name of a built-in error is fixed by the language standard. The wording of the message an engine writes for you is not: the same mistake is described differently by different JavaScript engines, and the wording changes between versions of the same engine.

So branch on the name and show the message to people. Testing error.name === "TypeError" is stable. Testing error.message against an exact sentence is a bug waiting for the next release.

The messages you write yourself are a different matter entirely. Those are yours, nothing changes them behind your back, and writing good ones is the subject of the next lesson.

Worked examples

Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.

The two parts of every error

Build an error by hand and look at it. An error is an object with a name and a message, and converting it to text joins the two.

1const failure = new TypeError("volume must be a number");2 3console.log(failure.name);4console.log(failure.message);5console.log(String(failure));6console.log(typeof failure);7console.log(failure instanceof Error);
  • line 1

    Nothing is thrown here. new TypeError(...) just builds an object, exactly like new Date() or a plain object literal would.

  • line 3

    name says which kind of failure this is. It is a plain string property, and it is the thing code should branch on.

  • line 4

    message is the sentence for a human. Here we wrote it ourselves, so we know its exact wording.

  • line 5

    Converting an error to text gives name, a colon, then the message. If the message is empty you get just the name.

  • line 6

    typeof reports object, because that is all an error is. There is no separate error type in the language.

  • line 7

    Every built-in error type is built on Error, so this check is true for TypeError, RangeError, ReferenceError and SyntaxError alike.

What it prints

TypeErrorvolume must be a numberTypeError: volume must be a numberobjecttrue

Which lines run, and which never do

Watch a throw skip everything after it. Two of the five log lines here never execute, and knowing exactly which two is the skill this example is teaching.

1function chargeCard(amountInPence) {2  console.log("1. checking the amount");3  if (typeof amountInPence !== "number") {4    throw new TypeError("chargeCard: amountInPence must be a number");5  }6  console.log("2. charging the card");7  return "charged";8}9 10try {11  const outcome = chargeCard("free");12  console.log("3. this line never runs");13  console.log(outcome);14} catch (error) {15  console.log("4. caught: " + error.message);16}
  • line 2

    This runs. The function was entered normally, and the throw is still two lines away.

  • line 4

    The alarm. Everything after this point inside chargeCard is skipped, including the log on line 6 and the return on line 7.

  • line 6

    Never runs for this call. There is no way to reach it once line 4 has thrown.

  • line 12

    Also never runs. The skipping does not stop at the edge of chargeCard: the rest of the try block is abandoned too.

  • line 15

    This is where control lands. error is the exact TypeError object that line 4 created.

What it prints

1. checking the amount4. caught: chargeCard: amountInPence must be a number

Provoking each of the four on purpose

Five deliberate mistakes, each reported by name. Reading this list once is worth more than memorising definitions, because these are the exact five you will actually cause.

1function nameOfFailure(action) {2  try {3    action();4    return "no error";5  } catch (error) {6    return error.name;7  }8}9 10console.log(nameOfFailure(function () { const nothing = null; return nothing.length; }));11console.log(nameOfFailure(function () { const five = 5; return five(); }));12console.log(nameOfFailure(function () { return neverDeclaredAnywhere; }));13console.log(nameOfFailure(function () { return JSON.parse("{oops}"); }));14console.log(nameOfFailure(function () { return (1).toFixed(200); }));
  • line 10

    Reading a property of null. The value is the wrong kind of thing to have properties at all, so: TypeError.

  • line 11

    Calling a number. Again the wrong kind of thing for the operation, so again TypeError.

  • line 12

    A name that was never declared: ReferenceError. Note that this only fires when the line runs, unlike a genuine syntax error.

  • line 13

    JSON.parse reports SyntaxError, because the text it was given was not JSON. This is the SyntaxError you will meet most often.

  • line 14

    A real number asked for an impossible number of decimal places: the right kind of thing, an unusable value, so RangeError.

What it prints

TypeErrorTypeErrorReferenceErrorSyntaxErrorRangeError

The failures that do not throw

Four operations that were asked something nonsensical and answered anyway. Nothing here stops, and that is precisely the problem.

1const typed = "twelve";2const parsed = Number(typed);3 4console.log(Number.isNaN(parsed));5console.log(typeof parsed);6console.log([1, 2, 3].indexOf(9));7 8const order = { id: "A-1" };9console.log(order.total);10console.log(typeof order.total);
  • line 4

    Number("twelve") produced NaN rather than throwing. We print Number.isNaN rather than the value itself, because the run pad renders NaN as null.

  • line 5

    NaN's type is number. It will pass any typeof check you write, which is exactly why typeof is not enough (lesson four).

  • line 6

    indexOf answers -1 for not found. That is a real position in no array, but it is a legal number and arithmetic will happily use it.

  • line 9

    A property that was never set reads as undefined. No error, no warning; the typo in a property name is one of the quietest bugs there is.

What it prints

truenumber-1undefinedundefined

Check yourself

Not scored, not stored. Getting one wrong is the useful part.

Your program stops and you print the caught error's name. It says ReferenceError. Which of these is the likely cause?

Reading the word twelve where a number was expected is a mistake. Does this program stop? Write down all three lines before you reveal them.

const total = Number("twelve");console.log(Number.isNaN(total));console.log(typeof total);console.log("still running");

A function logs a line, then throws, then logs a second line. The call is wrapped in try / catch. What gets printed?

Write it yourself

Exercise · intro · 9 tests

Summarise any failure in one line

Write a function named summariseError that turns anything a program might have thrown into one short line a person can read. Apply these rules in order. 1. If the value is not an Error, return "Unknown error: " followed by the value as text. JavaScript lets code throw a string, a number or anything else, so this case is real rather than theoretical. 2. If it is an Error whose message is empty, return the name on its own. 3. Otherwise return the name, then a colon and a space, then the message. Use the error's own name property. Code routinely reassigns it, and one of the hidden tests does exactly that.

What your code must do

summariseError accepts any value at all, always returns a string, and never throws. For an Error it reads the name and message properties. For anything else — a string, a number, null, a plain object — it returns "Unknown error: " followed by that value converted to text.

Define summariseError at the top level of your code — the tests call it by name.

Runs on your device in a sandbox with no network and no access to this page.

Worth remembering

  • What two properties does every built-in JavaScript error carry?

    name, saying what kind of failure it is, and message, a sentence for a human. Branch on name; show message to people. The wording of a message the engine wrote is not fixed by the standard and changes between engines and versions.

  • What happens to the lines after a throw?

    None of them run. Throwing leaves the current function immediately and keeps leaving outwards, caller by caller, until something catches the value or the program stops. Work already done before the throw is not undone.

Check this lesson against the source

We wrote the explanation; we did not invent the facts. This is the page that backs them.

MDN — Error

Check there: That Error instances carry name and message, that message for a user-created error is the string passed to the constructor, and that TypeError, RangeError, ReferenceError and SyntaxError are listed as Error subclasses.

Prefer to read the source in a structured breakdown? Turn this doc into a breakdown

0 of 1 exercise in this lesson solved on this device

Consent version 2026-07-31.1

Cookie preferences