Errors, validation, and defensive code · Lesson 47 of 77 · project

Failing loudly, not silently

About 45 minutes.

By the end of this lesson you can

  • Spot the three common ways JavaScript code fails without saying anything.
  • Write an assertion that turns an impossible state into an immediate, named error.
  • Log a failure with enough context to act on, without dumping the whole input.
  • Assemble validation, processing and reporting into one small pipeline.

Three silences

Explanation

The first silence is the empty catch block from lesson three. Something failed, something caught it, nothing said so.

The second is the plausible default. Number(item.price) || 0 turns a missing price, a null price and the text n/a into zero, and the total that comes out looks completely reasonable. So does ?? 0 and so does a fallback object. Defaults are not wrong — a default with no report is.

The third is the write that did nothing. Assign to a property of a frozen object outside strict mode and the assignment is simply ignored: no error, no warning, and the next line reads the old value. The same statement inside strict mode throws a TypeError. That is one of the strongest arguments for strict mode there is, and it is why every module and every class body is strict automatically.

All three share a shape. The program knew something, and chose to say nothing. Robust code is not code that never fails; it is code that never fails quietly.

Assertions: tripwires for the impossible

Explanation

An assertion is a check on something you believe cannot be false. Not a check on user input — a check on your own reasoning. An average of an empty list, a total that came out negative, a lookup that should always find something.

It is three lines: if the condition is false, throw an error saying which belief was wrong. There is no assert built into JavaScript in the browser, so you write it, and writing it takes about as long as reading this paragraph.

The value is in where it fires. Without the assertion, an impossible state travels quietly until it produces a visibly wrong number several functions later, and you debug backwards from the symptom. With it, the program stops at the first line that knew, naming the belief that turned out to be false.

Keep them apart from validation in your head. If an assertion fires, the correct response is to change the code. If validation fails, the correct response is to tell the user. An assertion message is never shown to a user, because it is a message about your program, not about their input.

A log line you can act on

Why it matters

A log line has one job: to let somebody who was not there work out what to do. That needs where it happened, what the rule was, and which record broke it — importRows row 12: quantity must be a whole number at least 1 has all three in a dozen words.

What it does not need is everything. Dumping the whole input drowns the useful part and copies data into a file with different access rules from the place it came from, which turns a logging decision into a privacy decision. Log the identifier and the rule, not the payload.

Count as well as describe. A run that says 340 imported, 2 skipped is answerable at a glance; one that says only skipped a row, twice, leaves the reader to work out whether that is normal. The report from this lesson's project does both, because both together are what make a failure actionable rather than merely visible.

Loud at the edge, calm inside

A mental model

Put all four of this module's tools on one picture and they stop competing. At the boundary, validate and report — every problem at once, as data, because the caller has a person to tell.

Just inside it, guard and throw. Anything wrong here is a bug in the calling code rather than a bad input, and stopping immediately is the kindest thing you can do to whoever wrote it.

Deeper in, assert and stay calm. Code well inside the boundary can assume its data is good, because something at the edge guaranteed it; assertions are there for the day that guarantee turns out to be false.

And everywhere: nothing fails without saying so. That single rule is what the module has been building towards, and the project below is the smallest complete example of it.

The shape of a robust function

Recap

Guard the arguments and throw for anything that means a bug in the caller.

Validate the data and collect every problem rather than stopping at the first.

Choose deliberately between throwing and returning a result, on the basis of who reads the message.

Handle only what you understand, re-throw the rest, and clean up in finally.

Report what happened — what worked, what did not, and why — so that the next person does not have to guess.

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 write that did nothing

The same assignment, to the same frozen object, twice. Outside strict mode it is ignored in silence; inside strict mode it throws. Nothing else differs.

1const settings = Object.freeze({ theme: "light" });2 3function sloppyUpdate() {4  settings.theme = "dark";5  return settings.theme;6}7 8function strictUpdate() {9  "use strict";10  try {11    settings.theme = "dark";12    return "changed to " + settings.theme;13  } catch (error) {14    return "refused: " + error.name;15  }16}17 18console.log(sloppyUpdate());19console.log(strictUpdate());
  • line 4

    This line does nothing at all. No error, no warning, no return value to check — the assignment is discarded.

  • line 5

    Reads back "light". A caller who believed the update worked now carries on with an object that was never changed.

  • line 9

    One directive, and the same assignment becomes a TypeError. Module code and class bodies are strict automatically, so this is the default in most modern files.

  • line 14

    Now there is something to catch, something to log and something to fix. Loud beats silent even when loud is inconvenient.

What it prints

lightrefused: TypeError

An assertion is a tripwire

Six lines of assert, and a function that now stops at the moment its assumption breaks rather than returning a nonsense number.

1function assert(condition, message) {2  if (!condition) {3    throw new Error("assertion failed: " + message);4  }5}6 7function averageOf(numbers) {8  assert(Array.isArray(numbers), "averageOf needs an array");9  assert(numbers.length > 0, "averageOf needs at least one number");10 11  let total = 0;12  for (const value of numbers) {13    total = total + value;14  }15  return total / numbers.length;16}17 18console.log(averageOf([2, 4, 6]));19try {20  averageOf([]);21} catch (error) {22  console.log(error.message);23}
  • line 3

    The whole implementation. There is no built-in assert in the browser, and there does not need to be.

  • line 9

    Without this line, averageOf([]) returns 0 divided by 0, which is NaN — a value that spreads through every later calculation without complaint.

  • line 15

    The body can be written plainly, because both assumptions above it have already been established.

  • line 20

    The failure names the belief that was wrong. Compare that with debugging backwards from a NaN that appeared on a dashboard.

What it prints

4assertion failed: averageOf needs at least one number

A total that is quietly wrong

Two totals over the same three rows. One is a plausible number nobody will question; the other stops and says which row is broken.

1const rows = [{ price: 10 }, { price: "12" }, { price: null }];2 3function quietTotal(items) {4  let total = 0;5  for (const item of items) {6    total = total + (Number(item.price) || 0);7  }8  return total;9}10 11function loudTotal(items) {12  let total = 0;13  for (const item of items) {14    if (!Number.isFinite(item.price)) {15      throw new TypeError(16        "loudTotal: price must be a number, received " + JSON.stringify(item.price),17      );18    }19    total = total + item.price;20  }21  return total;22}23 24console.log(quietTotal(rows));25try {26  loudTotal(rows);27} catch (error) {28  console.log(error.message);29}
  • line 6

    Two silences in one expression: Number converts the text "12" without comment, and || 0 turns the null row into zero.

  • line 24

    22 — a perfectly plausible total. Nothing about it suggests that one row was converted and another was invented.

  • line 14

    The loud version refuses to guess. Number.isFinite rejects the text and the null alike, because neither is a price.

  • line 16

    The message names the function and quotes the offending value, so the reader can find the row without opening a debugger.

What it prints

22loudTotal: price must be a number, received "12"

Check yourself

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

A frozen object is assigned to outside strict mode. Does this program stop? What are the two lines?

const config = Object.freeze({ retries: 3 });config.retries = 10;console.log(config.retries);console.log(Object.isFrozen(config));

What separates an assertion from validation?

An import loop rejects one row out of 400. What belongs in the log line?

Write it yourself

Exercise · core · 12 tests

Project: an import that reports what it did

This is the module's project: one function that guards, validates, processes and reports. importRows(rows) takes an array of stock rows and returns a report object with three properties: imported (a number), skipped (a number) and problems (an array of strings). - If rows is not an array, throw a TypeError whose message starts with "importRows: ". Nothing else in this function throws. - A row is usable when it is a plain object, its sku is a string with at least one non-space character, and its quantity is a whole number of at least 1. - Count every usable row in imported, and every unusable row in skipped. - For each unusable row, push one message of the form "row 3: <reason>", numbering rows from 1 and counting every row including the usable ones. The reason is exactly one of "not an object", "sku must not be empty" or "quantity must be a whole number at least 1", checked in that order, reporting only the first that applies.

What your code must do

importRows always returns a report in which imported plus skipped equals the number of rows given, and problems has exactly one entry per skipped row, in row order. Row numbers are 1-based and count every row. An empty array gives imported 0, skipped 0 and no problems. Only a non-array argument throws.

Define importRows 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.

Exercise · stretch · 8 tests

Give the pipeline a voice

The last thing a robust pipeline needs is a voice. Write runStep(name, work), which runs one step of a process and says what happened. - Call work with no arguments. - If it returns, log exactly "ok: " followed by the name, then return whatever it returned. - If it throws, log exactly "FAILED " then the name, then ": ", then the failure's message — the message property when an Error was thrown, and the value as text when it was not — and then re-throw the original value unchanged. Use console.log for both lines. The point of re-throwing is that runStep reports; it does not decide. Deciding is the caller's job.

What your code must do

runStep logs exactly one line per call and never swallows a failure. On success it returns the work function's own return value, including undefined. On failure the value that escapes runStep is the identical value that work threw — not a copy, not a wrapper.

Define runStep 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

  • Name the three ways JavaScript code fails without telling you.

    An empty catch block; a default such as || 0 or ?? 0 that replaces the reason with a plausible answer; and a write that does nothing, such as assigning to a frozen property outside strict mode.

  • What is the difference between an assertion and validation?

    Validation checks input that can legitimately be wrong and reports it to a user. An assertion checks something you believe can never be false, so it firing means a bug in your own code — and its message is never shown to a user.

  • What belongs in a log line about a failure?

    Where it happened, which record it was, and which rule broke — plus the totals for the run. Not the whole payload: that buries the useful part and copies data into a file with different access rules from its source.

Check this lesson against the source

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

MDN — Object.freeze()

Check there: That attempting to change a frozen object's property fails silently in non-strict code and throws a TypeError in strict mode, which is the third silence this lesson names.

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

0 of 2 exercises in this lesson solved on this device

Consent version 2026-07-31.1

Cookie preferences