← Fundamentals
Guided lesson 6 questions · 6 languages

Panics, traps, and undefined behavior

What a language promises after a bad operation: a defined stop, a choice left to the build, or no promise at all.

After a bad operation, such as reading past the end of a list, a language either stops the program with a message, gives a defined but useless value, or, in C, promises nothing. A bad operation means a bug; a problem the caller should expect belongs in an error value instead.

Before you begin

Why this matters

A bug that does not stop the program at once shows up later, far from its cause. Knowing which operations throw, and which quietly give undefined or NaN, tells you where to look.

What this helps you reason about

  • Predict which bad operations throw a TypeError or a RangeError, and which give undefined, NaN, or Infinity.
  • Find the line that caused a TypeError, even when it is not the line that threw.
  • Decide when a function should throw and when it should return a failure as a value.

A play has four scenes, and a line of code asks for the fifth. In many languages that read stops the program. In TypeScript it gives undefined, and the program goes on until a later line fails. This lesson shows what each bad operation does, and when a program should stop.

Already know this? The short version
  • An index past the end of an array, or a missing property, gives undefined, and arithmetic with it gives NaN; nothing throws until code uses undefined as an object.
  • The runtime throws a TypeError for an operation on the wrong kind of value, such as reading a property of undefined, and a RangeError for a number outside an allowed range, such as "-".repeat(-1).
  • Division by zero gives Infinity or NaN, never an error, and console.assert writes to standard error without stopping anything.
  • JavaScript has no undefined behavior: every operation gives a defined result or throws.
  • Throw for a bug and let one handler at the top report it; return a failure as a value for a problem the caller should expect.

Go to the final check· Go to the practice tickets

Words in this lesson (7)
bug
A mistake in a program’s own code, as opposed to a problem in its input or its surroundings. Only a change to the code fixes it.
undefined
A special JavaScript value that means “no value yet”. A let variable declared without a value holds undefined.
NaN
“Not a number”: the floating-point value that results from a calculation with no answer, such as 0 / 0. NaN is not equal to anything, not even itself.
exception
A value that is thrown to stop the normal flow when an operation fails. In TypeScript, any value can be thrown, but code usually throws an Error object, which has a message.
TypeError
The error JavaScript throws when an operation gets a value it cannot work with, such as reading a property of undefined.
RangeError
The error JavaScript throws when a number is outside the range an operation accepts, such as a negative count for repeat.
assertion
A check written into the program that a condition is true at that point. JavaScript has no assert statement: console.assert only prints a message when the condition is false, and a check that must stop the program throws an Error.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Bad operations

What happens after a bad operation?

A bad operation is an operation whose input has no sensible result: a position past the end of a list, a missing value used as if it were there, an integer divided by zero. It almost always means a bug: a mistake in the program’s own code. After a bad operation, a language does one of three things. It stops the program at that line with a message; this is often called a trap. It gives a defined result that is not useful, such as an empty value. Or, as in C, it promises nothing at all.

TypeScript often takes the second way. scenes[4], on a list of four scenes, gives undefined. minutes * 60 then gives NaN, “not a number”. No error appears. The program stops only when a later line uses undefined as an object, for example by calling a method on it. That line throws a TypeError, and the bug is on an earlier line.

main.ts
const scenes = [12, 8, 15, 10];
const minutes = scenes[4];
console.log(minutes); // undefined
console.log(minutes * 60); // NaN
console.log(minutes.toFixed(1));
// TypeError: Cannot read properties of undefined (reading 'toFixed')

In TypeScript, an index past the end gives undefined; the program fails later, where undefined is used as an object.

Coming from Python: Python raises IndexError for scenes[4]; JavaScript gives undefined and continues.

Coming from Java: There is no ArrayIndexOutOfBoundsException: an index past the end of an array gives undefined.

Precisely: noUncheckedIndexedAccess

With the compiler option noUncheckedIndexedAccess, the type of scenes[4] is number | undefined, and the compiler refuses minutes.toFixed(1) until the code has tested minutes. The stepper does not turn the option on, so the program above compiles.

Experiment

Read a scene past the end of the list

An index past the end of an array is not an error in JavaScript; it gives undefined. undefined * 60 gives NaN, also without an error. Calling a method on undefined has no result at all, so the runtime throws a TypeError at that line.

Why it matters When a TypeError names undefined, the bug is usually on an earlier line: look for the line that produced undefined.

Predict every printed line: does any line fail? Then add the line console.log("rounded: " + minutes.toFixed(1)); just above the last line, and predict again.

const scenes = [12, 8, 15, 10];
const position = 4;
console.log("curtain up");
const minutes = scenes[position];
console.log("scene " + (position + 1) + ": " + minutes);
console.log("in seconds: " + minutes * 60);
console.log("curtain down");
What to look for

Step from the top. A const that has not run yet shows as “not initialized yet”, and the stepper sometimes stops on one line twice; both are normal. After the line const minutes = scenes[position];, minutes shows undefined. The run prints curtain up, scene 5: undefined, in seconds: NaN, and curtain down: no line fails. After the edit, the run stops at the new line with TypeError: Cannot read properties of undefined (reading 'toFixed'), and curtain down is never printed.

Expected output

curtain up
scene 5: undefined
in seconds: NaN
curtain down

02 / Bugs

Which failures mean a bug in the code?

The JavaScript runtime throws an error for some bad operations. A TypeError means that an operation got a value it cannot work with: reading a property of undefined or null, or calling something that is not a function. A RangeError means that a number is outside the range an operation accepts: "-".repeat(-1), new Array(-1), or calls nested so deeply that the call stack runs out.

Many other bad operations throw nothing. 12 / 0 is Infinity, 0 / 0 is NaN, and a missing property is undefined. These values flow on into later results, and the program never stops at the line that made them.

main.ts
const scenes = [12, 8, 15, 10];
console.log(12 / 0); // Infinity
console.log("-".repeat(scenes.length - 5));
// RangeError: Invalid count value: -1

TypeError and RangeError stop the program at the bad operation; undefined, NaN, and Infinity do not stop it at all.

Precisely: whose message it is

The specification fixes which error is thrown, such as a TypeError, but not its message. The messages quoted here come from V8, the JavaScript engine in Node.js 22; another engine words them differently.

03 / Build choices

What is left to the compiler or the build?

Some behavior is not fixed by the language but by the tool that runs it. Two examples matter here. The text of an error message belongs to the engine, so code should test the error’s type, not its message. And console.assert(condition, message) never stops a program. When the condition is false, Node.js writes the message to standard error, and the program continues with the next line. The stepper does not show standard error, so on screen nothing happens at all.

To stop on a broken rule, test it and throw.

main.ts
const position = 4;
// Writes to standard error, then continues:
console.assert(position < 4, "no such scene");
// Stops the program here:
if (position >= 4) {
  throw new Error("no such scene");
}

console.assert only reports; to stop on a broken rule, throw.

Experiment

Check a rule with console.assert

console.assert is a function that writes a message when its condition is false, and then returns like any other call. throw is the statement that stops the normal flow.

Why it matters console.assert only reports; a rule that must stop the program needs a throw.

Predict what is printed: does console.assert stop the program? Then replace the console.assert line with if (position >= scenes.length) throw new Error("no such scene"); and predict again.

const scenes = [12, 8, 15, 10];
const position = 4;
console.assert(position < scenes.length, "no such scene");
console.log("checked scene " + (position + 1));
console.log("minutes: " + scenes[position]);
What to look for

The stepper stops on the console.assert line, then moves on to the next line. The run prints checked scene 5, then minutes: undefined. console.assert did write a message, but to standard error, which the stepper does not show. After the edit, the stepper stops on the new line, nothing is printed, and the run ends with Error: no such scene at line 3.

Expected output

checked scene 5
minutes: undefined

04 / No promise

What does undefined behavior mean?

In C, some bad operations have undefined behavior: the language sets no rule at all for what happens next, and the program may print anything. JavaScript has none. Every operation either gives a defined result, such as undefined or NaN, or throws. The specification says which, so the same program does the same thing in every engine.

That is a promise about the language, not about your program. A defined result can still be a wrong one: NaN in a total is defined, and it is still a bug.

main.ts
const total = 40 + [12, 8][5]; // NaN, in every engine

JavaScript has no undefined behavior, but a defined result can still be a wrong one.

05 / Stop or report

When should code stop, and when should it return an error?

A bug and an expected problem need different handling. A bug is a mistake in the code, such as a position past the end, or a value that the code itself should have set. Only a change to the code fixes it, so the best a running program can do is stop, with a clear message, before it shows a wrong answer. An expected problem comes from outside the code: a title typed wrong, a file that is missing. The caller can do something about it, so it should arrive as a value that the caller must look at.

In TypeScript, both kinds travel the same way if you let them, so the difference is in what you write. Return undefined, or an object such as { ok: false, reason }, for an expected problem, as Errors as values or exceptions showed. Throw for a bug, and let it travel up to one handler at the top of the program that reports it. A catch close to the call is right only for an expected problem, and only for the error class you expect; a catch that takes everything turns a bug into wrong data.

main.ts
// A number a user typed: the caller decides.
function sceneMinutes(scenes: number[], n: number) {
  if (n < 1 || n > scenes.length) {
    return undefined;
  }
  return scenes[n - 1];
}

// Called only with a plan that this code built.
function opening(scenes: number[]): number {
  if (scenes.length === 0) {
    throw new Error("bug: a plan with no scenes");
  }
  return scenes[0];
}

Return a value for a problem the caller should expect; throw for a bug, and catch it once, at the top.

Coming from Go: TypeScript has no panic: a bug and an expected failure can both be thrown, so the difference lies in what you throw and where you catch it.

06 / Try it

Can you explain a fresh example?

Decide for each line whether it gives a value, gives a defined but useless value, or stops the program. I/O, buffering, and external state, a later lesson, shows what happens to output that is still waiting when a program stops.

main.ts
const rows = [3, 0, 7];
console.log(rows[5]);
console.log(12 / rows[1]);
try {
  console.log("-".repeat(rows[1] - 1));
} catch (problem) {
  console.log("caught " + String(problem));
}
console.log("end");

What is printed, line by line?

Reveal the reasoning

undefined, Infinity, caught RangeError: Invalid count value: -1, end. rows[5] is past the end, which gives undefined. 12 / 0 gives Infinity; division never throws. rows[1] - 1 is -1, and repeat refuses a negative count with a RangeError, which the catch block prints. The program then continues after the try statement.

Practice tickets

  1. PANU-101 · The festival screen breaks while the last act plays → Free
  2. PANU-102 · A mistyped tempo is never reported as invalid → Sign in · Pro
  3. PANU-103 · The set timer shows a time for songs that are not on the list → Sign in · Pro

Keep exploring

I/O, buffering, and external state →

Primary references