← Fundamentals
Guided lesson 5 questions · 6 languages

Short-circuit evaluation

Which side of && and || is skipped, how a guard uses that, and what the result is.

&& and || compute their left side first. When the left side already decides the answer, the right side is never computed: && skips it when the left side is false, and || skips it when the left side is true. So the left side can protect the right side, and a call on the right side may never run.

Before you begin

Why this matters

Whether the right side of && or || runs decides whether a function is called, whether a read on an empty list stops the program, and whether a counter is updated. In TypeScript, && and || also give back one of their values, not always true or false.

What this helps you reason about

  • Predict whether a call on the right side of && or || runs.
  • Write a test that protects a read, in the right order, and spot one in the wrong order.
  • Predict the value of name && name.length or count || 10, and choose ?? or ?. where they fit.

A vaccine clinic calls the next patient only when it has doses left. Written with &&, that rule also decides whether the call happens at all. This lesson shows which side of && and || TypeScript skips, and what value the whole expression gives.

Already know this? The short version
  • a && b computes b only when a is truthy; a || b computes b only when a is falsy; a ?? b computes b only when a is undefined or null.
  • A test on the left protects an expression on the right: waiting.length > 0 && waiting[0].age < 12. Swapped, waiting[0].age throws a TypeError on an empty array; TypeScript catches the swapped order for a value typed with | undefined, but not for an array read.
  • A call on the right side that counts or logs runs only sometimes; call it on its own line first. TypeScript refuses & on two booleans.
  • && and || give one of their two values, not always a boolean: "" && x gives "", count || 10 replaces a real 0, and a?.b gives undefined instead of throwing.

Go to the final check· Go to the practice tickets

Words in this lesson (6)
short-circuit evaluation
Computing && or || from left to right and stopping as soon as the left side decides the result. The right side is then not computed at all.
guard
A test on the left of && that makes the right side safe to compute, such as checking that a list is not empty before reading its first element.
side effect
A change that code makes besides giving its value, such as changing a variable or printing a line.
truthy and falsy
A value is falsy when a condition treats it as false: false, 0, -0, 0n, "", NaN, null, and undefined. Every other value is truthy, including "0" and an empty array [].
nullish coalescing (??)
The operator a ?? b. It gives b only when a is undefined or null, so it keeps "" and 0. a || b gives b whenever a is falsy, including "" and 0.
optional chaining (?.)
When a is undefined or null, a?.b gives undefined and does not read b. Otherwise it reads a.b.

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 / Skipping

Which side do && and || skip?

Conditions and truthiness combined tests with && and ||, and gave short-circuit evaluation a name. Here is what it means. Both operators compute their left side first. When the left side already decides the result, the right side is not computed at all.

For &&, a falsy left side decides: falsy && anything is falsy. So a && b computes b only when a is truthy. For ||, a truthy left side decides: truthy || anything is truthy. So a || b computes b only when a is falsy.

When the right side is a function call, “not computed” means that the function is never called: its body does not run. Below, doses > 0 is falsy, so callNext is never called, and nothing about the next patient is printed.

main.ts
function callNext(): boolean {
  console.log("calling the next patient");
  return true;
}
let doses = 0;
let called = doses > 0 && callNext(); // callNext is not called
console.log("called: " + called); // called: false

a && b computes b only when a is truthy; a || b computes b only when a is falsy.

Experiment

Watch && skip a call on its right side

&& computes its left side first. doses > 0 is false, and false && anything is false, so the right side is never computed and callNext is not called. When the left side is true, the answer depends on the right side, so callNext runs. This debugger sometimes stops on the same line twice; that is normal.

Why it matters A call on the right side of && runs only when the left side is true. On the right side of ||, it runs only when the left side is false.

Predict what is printed, and whether the stepper ever goes into callNext. Then change let doses = 0; to let doses = 3; and predict again.

function callNext(): boolean {
  console.log("calling the next patient");
  return true;
}
let doses = 0;
let called = doses > 0 && callNext();
console.log("called: " + called);
What to look for

At the line with &&, doses shows 0. The next stop is the last line: the stepper never goes into callNext, and the run prints called: false. After the edit, the stepper goes from the && line into callNext, and the run prints calling the next patient, then called: true.

Expected output

called: false

02 / Protecting

How can the left side protect the right side?

Short-circuiting lets the left side protect the right side. Below, waiting is an array: a list of values in order, written with brackets. waiting[0] is its first element, because positions start at 0, and waiting.length is its number of elements. waiting[0].age reads the age of the first patient. On an empty list there is no first patient, and waiting[0] gives undefined, the JavaScript value that means “no value”. Reading .age of undefined stops the program with a TypeError, the error JavaScript throws when a value is used in a way its type does not allow. Put a test first that makes the read safe: waiting.length > 0 && waiting[0].age < 12. When the list is empty, the left side is false, and the read never happens. A test used this way is called a guard.

The order matters. With the two sides swapped, the read comes first and the guard comes too late.

The same pattern protects a value that may be missing. ben.note !== undefined && ben.note.includes("egg") reads the note only when there is one. includes asks whether a string contains another string.

main.ts
type Patient = { name: string; age: number };
let waiting: Patient[] = [];
if (waiting.length > 0 && waiting[0].age < 12) {
  console.log("a child is first");
}
// waiting[0].age < 12 && waiting.length > 0
// stops: Cannot read properties of undefined

A guard goes on the left of &&, and the expression it protects goes on the right.

Coming from Python: and and or skip their right side the same way; && and || are their TypeScript spellings.

Precisely: when TypeScript checks the order

For a value whose type includes undefined, such as a property declared note?: string, TypeScript checks the order for you. ben.note.includes("egg") && ben.note !== undefined does not compile: 'ben.note' is possibly 'undefined'. The test on the left narrows the type for the right side, so only the guarded order compiles. An array read is not checked this way: waiting[0] has the type Patient even when the list is empty. The compiler option noUncheckedIndexedAccess adds undefined to every array read, which makes the compiler require a guard there too.

Experiment

Swap a guard and the read it protects

The guard is computed first. It is false for an empty list, so && never reads waiting[0].age. With the sides swapped, waiting[0] is read first; it is undefined, and undefined has no age to read.

Why it matters Put the test that makes a read safe on the left of &&, and the read on its right.

Predict what is printed for an empty waiting list. Then swap the two sides of &&, so that the condition reads waiting[0].age < 12 && waiting.length > 0, and predict again.

type Patient = { name: string; age: number };
let waiting: Patient[] = [];
if (waiting.length > 0 && waiting[0].age < 12) {
  console.log("a child is first");
} else {
  console.log("no child first");
}
What to look for

At the if line, waiting shows []. The next stop is the "no child first" line, and the run prints no child first. After the edit, the run stops at the if line with TypeError: Cannot read properties of undefined (reading 'age'), and nothing is printed.

Expected output

no child first

03 / Side effects

What if the skipped side does something?

A call on the right side runs only when the left side lets it. That matters when the call does something besides giving its result: printing a line, adding 1 to a counter, saving a record. Such a change is called a side effect.

Below, recordFound adds 1 to checked each time it is called, so the clinic can count the records it checked today. The patient is 15, so age >= 16 is false, and recordFound is never called. checked stays 0. The count is wrong, and nothing reports an error.

When a call must run every time, do not put it on the right side of && or ||. Call it on its own line, keep its result in a variable, and combine the variable: let found = recordFound(); then if (age >= 16 && found).

TypeScript has no version of && that always computes both sides. a & b with two booleans is refused: The '&' operator is not allowed for boolean types. Consider using '&&' instead.

main.ts
let checked = 0;
function recordFound(): boolean {
  checked = checked + 1; // a side effect
  return true;
}
let age = 15;
let found = recordFound(); // runs every time
if (age >= 16 && found) {
  console.log("vaccine due");
}

A side effect on the right side of && or || runs only sometimes. When it must always run, call it first, on its own line.

Experiment

Count records checked on the right side of &&

age >= 16 is false, so && never calls recordFound, and the line that adds 1 to checked never runs. On its own line, the call runs every time, and && combines two values that are already computed. vaccine due is printed in neither run, because the patient is 15.

Why it matters When a call must run every time, do not put it on the right side of && or ||. Call it on its own line first.

Predict what records checked prints for a 15-year-old patient. Then add the line let found = recordFound(); before the if, change the condition to age >= 16 && found, and predict again.

let checked = 0;
function recordFound(): boolean {
  checked = checked + 1;
  return true;
}
let age = 15;
if (age >= 16 && recordFound()) {
  console.log("vaccine due");
}
console.log("records checked: " + checked);
What to look for

At the if line, age shows 15 and checked shows 0. The stepper never goes into recordFound, and the run prints records checked: 0. After the edit, the stepper goes into recordFound, checked changes to 1, and the run prints records checked: 1.

Expected output

records checked: 0

04 / Result

Can the result be something other than a boolean?

In TypeScript, && and || do not always give a boolean. They give the value of one of their two sides, unchanged. a && b gives a when a is falsy, and b otherwise. a || b gives a when a is truthy, and b otherwise.

So patient && patient.length gives "" for an empty name and 3 for "Ana". doses || 10 gives 10 when doses is 0, because 0 is falsy. Conditions and truthiness showed the safer default for that case, doses ?? 10, which replaces only undefined and null. ?? short-circuits too: a ?? b computes b only when a is undefined or null.

Optional chaining, ?., skips work in the same way. When a is undefined or null, a?.b gives undefined and does not read b. Otherwise it reads a.b. In the type below, note?: string means the note may be missing. ben.note?.includes("egg") gives undefined for a patient with no note, instead of stopping the program.

main.ts
let doses = 0;
console.log(doses || 10); // 10: 0 is falsy
console.log(doses ?? 10); // 0
let patient = "Ana";
console.log(patient && patient.length); // 3
type Patient = { name: string; note?: string };
let ben: Patient = { name: "Ben" };
console.log(ben.note?.includes("egg")); // undefined

&& and || give one of their two values, not always true or false; ?? and ?. skip their right side when the left side is undefined or null.

Coming from Python: and and or also give one of their two values: "" or "x" is "x" in both languages.

Coming from Java or C#: TypeScript’s && can give a number or a string. Compare first, as in count > 0 && …, when you need true or false.

Precisely: the rule in the specification

The ECMAScript specification defines a && b as: evaluate a; if ToBoolean(a) is false, return a; otherwise evaluate b and return it. a || b is the same with the test reversed. Only the decision uses ToBoolean, the truthiness rule; the value returned is the original one. ?. stops the rest of its chain: in a?.b.c, when a is undefined, .c is not read either.

05 / Try it

Can you explain a fresh example?

Decide for each line whether the left side already decides the result, then count the calls. The next lesson, Recursion, uses a test like these to decide when a function stops calling itself.

main.ts
let calls = 0;
function inStock(): boolean {
  calls = calls + 1;
  return true;
}
let doses = 4;
let a = doses > 5 && inStock();
let b = doses > 2 || inStock();
let c = doses > 2 && inStock();
let label = doses && "open";
console.log(a, b, c, calls, label);

What is printed?

Reveal the reasoning

false true true 1 open. For a, doses > 5 is false, so && skips the call. For b, doses > 2 is true, so || skips the call. For c, doses > 2 is true, so && needs the right side, and inStock runs once. calls is 1. For label, 4 is truthy, so && gives its right side, the string "open", not true.

Practice tickets

  1. SHORT-101 · The early-opening check fails on days with no appointments → Free
  2. SHORT-102 · A plan with several problems shows only the first one → Sign in · Pro
  3. SHORT-103 · First-time blood donors are always turned away → Sign in · Pro

Keep exploring

Recursion →

Primary references