Conditions let a program choose what to do: if runs a block only when an expression is true, else if and else cover other cases, and switch is handy for choosing by a single value. Loops repeat an action: for a set number of times, for...of for every array element, while as long as a condition holds. Every loop needs an exit condition, or the program hangs.
After this you will be able to
- List every falsy value in JavaScript and predict how a value behaves in a condition
- Write branching with if / else if / else, the ternary operator and switch, and choose between switch and a lookup object
- Iterate over an array with for, for...of and while loops and control them with break and continue
- Find and fix an off-by-one error and an infinite loop
Know beforehand
Verified with
Node.js 24 LTS (24.18.0)Microsoft Edge 155
Checked against docs: October 11, 2026
Contents
Logical expressions and truthy/falsy
Every decision in a program starts with a yes-or-no question: is the total above 15,000? is the item out of stock? is the field filled in? In code, that question is a logical expression and its answer is true or false. You get them from comparison operators (===, !==, >, <, >=, <=) and logical operators: && for “and”, || for “or”, ! for “not”.
A condition doesn't have to receive exactly true or false. Put a number or a string in an if and JavaScript converts it to a boolean itself. According to MDN, exactly eight values are falsy: false, 0, -0, 0n, the empty string "", null, undefined and NaN (plus the exotic document.all in browsers). Every other value is truthy, including the string "0", a string with a single space and an empty array.
// Falsy values
console.log(Boolean(0)); // → false
console.log(Boolean("")); // → false
console.log(Boolean(null)); // → false
console.log(Boolean(undefined)); // → false
console.log(Boolean(NaN)); // → false
// Everything else is truthy, even things that "look empty"
console.log(Boolean("0")); // → true
console.log(Boolean(" ")); // → true
console.log(Boolean([])); // → true
// Comparisons and logical operators produce true or false
const age = 20;
const hasTicket = true;
console.log(age >= 18 && hasTicket); // → true
console.log(age < 18 || !hasTicket); // → falseif, else if, else and the ternary operator
if checks the condition in parentheses and runs the block in braces if it's true. else if adds another check, and else runs when none of the options matched. Checks go top to bottom and only the first matching branch runs, which is why larger thresholds go above smaller ones. MDN recommends always using braces, even for a one-line branch: it makes adding code easier and mistakes less likely.
const total = 23500; // order total, ₸
let discount;
if (total >= 50000) {
discount = 10;
} else if (total >= 20000) {
discount = 5;
} else {
discount = 0;
}
console.log(`Discount: ${discount}%`); // → Discount: 5%
// The ternary operator: condition ? if yes : if no
const delivery = total >= 15000 ? 0 : 1500;
console.log(`Delivery: ${delivery} ₸`); // → Delivery: 0 ₸The ternary operator condition ? value1 : value2 is a short form for picking one of two values. Chains of ternaries are technically possible but read worse than if…else if: for three or more options, use regular branches.
Now back to the tip calculator from the first lesson. Without arguments it printed NaN; let's fix that with a condition. The percentage also becomes optional: the ?? operator substitutes 10 if no argument is passed. process.exitCode = 1 tells the system the program finished with an error, which matters when other programs run your scripts.
// tip.js — tip calculator with input validation
// Usage: node tip.js <bill amount> [percent, default 10]
const bill = Number(process.argv[2]);
const percent = Number(process.argv[3] ?? 10);
if (Number.isNaN(bill) || bill <= 0) {
console.log("Enter the bill as a number greater than zero, e.g.: node tip.js 12500");
process.exitCode = 1;
} else {
const tip = Math.round(bill * percent / 100);
console.log(`Tip ${percent}%: ${tip} ₸`);
console.log(`Total: ${bill + tip} ₸`);
}$ node tip.js 12500
Tip 10%: 1250 ₸
Total: 13750 ₸
$ node tip.js 12500 0
Tip 0%: 0 ₸
Total: 12500 ₸
$ node tip.js
Enter the bill as a number greater than zero, e.g.: node tip.js 12500Why ?? and not the familiar ||? || returns the right-hand value whenever the left one is falsy, and 0 is falsy. A guest who deliberately entered a 0% tip would get 10%. ?? uses the default only for null and undefined.
const input = "0"; // the guest entered a 0% tip
const withOr = Number(input) || 10; // 0 is falsy, so || substitutes 10
const withNullish = Number(input ?? 10); // ?? only kicks in for null and undefined
console.log(withOr); // → 10
console.log(withNullish); // → 0switch, and when a lookup object is better
When you need to choose an action by a single value (an order status, a weekday, a payment type), a long else if chain gets hard to read. switch evaluates an expression and looks for the first case with the same value; per MDN, the comparison is strict, like ===. The default block runs if nothing matches.
const status = "paid";
switch (status) {
case "new":
console.log("Order received");
break;
case "paid":
console.log("Paid, packing now");
break;
case "shipped":
console.log("Handed to the courier");
break;
default:
console.log("Unknown status");
}
// → Paid, packing now
// The same thing with a lookup object
const labels = {
new: "Order received",
paid: "Paid, packing now",
shipped: "Handed to the courier",
};
console.log(labels[status] ?? "Unknown status"); // → Paid, packing now
console.log(labels.lost ?? "Unknown status"); // → Unknown statusswitch
- Fits when each branch does something different
- Several cases can share one branch
- Easy to forget break
Lookup object
- Fits when each value maps to data: a label, a price, an icon
- Shorter and easy to extend: one new line in the object
- The default value comes from ??
One subtlety with lookup objects: a plain object has inherited properties such as toString, so labels["toString"] returns a function, not undefined. If the key comes from user input, check it with Object.hasOwn(labels, key) or store the lookup in a Map; the data structures lesson covers this in more detail.
for, for...of and while loops
A loop repeats a block of code while a condition holds. Each repetition is called an iteration. JavaScript has several kinds of loops, but three are enough to start with.
// for — when you know how many times to repeat
for (let i = 1; i <= 3; i++) {
console.log(`Attempt ${i}`);
}
// → Attempt 1
// → Attempt 2
// → Attempt 3
// for...of — go through every element of an array
const items = [
{ name: "Cappuccino", price: 1600, qty: 2 },
{ name: "Croissant", price: 1200, qty: 1 },
{ name: "Water", price: 500, qty: 3 },
];
let sum = 0;
for (const item of items) {
sum += item.price * item.qty;
}
console.log(`Total: ${sum} ₸`); // → Total: 5900 ₸
// while — when the number of repeats isn't known up front
let saved = 0;
let weeks = 0;
while (saved < 10000) {
saved += 2500;
weeks++;
}
console.log(`Weeks: ${weeks}`); // → Weeks: 4for (let i = 1; i <= 3; i++)
- How it works
- Initial value; condition to continue; step after each iteration
- When to use
- Repeat a set number of times, or you need the step number
for (const item of items)
- How it works
- On each iteration item is the next element
- When to use
- Go through every array element or every character of a string
while (condition)
- How it works
- Checks the condition before each iteration
- When to use
- The number of repeats isn't known up front: saving up, waiting, searching
do { … } while (condition)
- How it works
- Runs the block first, then checks the condition
- When to use
- The body must run at least once
i++ is shorthand for i = i + 1. In for...of, declare the variable with const: a new one is created on each iteration. There's also a similar-looking for...in loop, but it goes over an object's property names rather than values; for arrays, use for...of.
break, continue and the exit condition
Sometimes you need to change a loop's course from the inside. continue ends the current iteration and moves on to the next one, which is handy for skipping elements that don't fit. break leaves the loop immediately, for when you've found what you need and there's no point going on.
const menu = [
{ name: "Cappuccino", left: 12 },
{ name: "Latte", left: 0 },
{ name: "Raf", left: 4 },
{ name: "Cocoa", left: 0 },
];
// continue — skip the current step and move on to the next
for (const drink of menu) {
if (drink.left === 0) {
continue;
}
console.log(`In stock: ${drink.name}`);
}
// → In stock: Cappuccino
// → In stock: Raf
// break — leave the loop immediately
let firstMissing = null;
for (const drink of menu) {
if (drink.left === 0) {
firstMissing = drink.name;
break;
}
}
console.log(`First to run out: ${firstMissing}`); // → First to run out: LatteThe key rule of any loop is the exit condition. MDN warns explicitly: make sure the loop condition eventually becomes false. If you forget to change the variable the condition depends on, the loop never ends:
let i = 0;
while (i < 3) {
console.log(i);
// forgot i++ — the condition never becomes false
}Practice: FizzBuzz and an order total
FizzBuzz is the classic exercise for conditions inside a loop: for the numbers 1 to 15, print “Fizz” instead of multiples of three, “Buzz” instead of multiples of five and “FizzBuzz” instead of multiples of both. Divisibility is checked with the remainder operator %: n % 3 === 0 means “divides by 3 with no remainder”.
for (let n = 1; n <= 15; n++) {
if (n % 15 === 0) {
console.log("FizzBuzz");
} else if (n % 3 === 0) {
console.log("Fizz");
} else if (n % 5 === 0) {
console.log("Buzz");
} else {
console.log(n);
}
}
// → 1
// → 2
// → Fizz
// → 4
// → Buzz
// → Fizz
// → 7
// → 8
// → Fizz
// → Buzz
// → 11
// → Fizz
// → 13
// → 14
// → FizzBuzzNow a task closer to real work: calculate a coffee shop order total. Skip lines with zero quantity, give a 5% discount from 10,000 ₸ and free delivery from 15,000 ₸.
const cart = [
{ name: "Cappuccino", price: 1600, qty: 2 },
{ name: "Cheesecake", price: 2400, qty: 0 }, // removed from the cart
{ name: "Beans 250 g", price: 6900, qty: 1 },
{ name: "Croissant", price: 1200, qty: 3 },
];
let subtotal = 0;
let positions = 0;
for (const item of cart) {
if (item.qty <= 0) {
continue;
}
subtotal += item.price * item.qty;
positions++;
}
const discount = subtotal >= 10000 ? Math.round(subtotal * 0.05) : 0;
const delivery = subtotal >= 15000 ? 0 : 1500;
const total = subtotal - discount + delivery;
console.log(`Items: ${positions}`); // → Items: 3
console.log(`Subtotal: ${subtotal} ₸`); // → Subtotal: 13700 ₸
console.log(`Discount: ${discount} ₸`); // → Discount: 685 ₸
console.log(`Delivery: ${delivery} ₸`); // → Delivery: 1500 ₸
console.log(`To pay: ${total} ₸`); // → To pay: 14515 ₸Note that the free delivery threshold is checked against the subtotal before the discount. That's a business rule, not a language rule; clarify such things with the client before writing the code.
Common mistakes: off-by-one and infinite loops
The off-by-one error is the most common loop bug. Array elements are numbered from zero, so a two-element array has indexes 0 and 1, while items.length is 2. The condition i <= items.length adds an extra iteration: items[2] doesn't exist.
const items = [
{ name: "Cappuccino", price: 1600 },
{ name: "Croissant", price: 1200 },
];
let sum = 0;
for (let i = 0; i <= items.length; i++) { // bug: should be i < items.length
sum += items[i].price;
}
console.log(sum);$ node off-by-one.js
…\first-js\off-by-one.js:8
sum += items[i].price;
^
TypeError: Cannot read properties of undefined (reading 'price')
at …
Node.js v24.18.0Cannot read properties of undefined
- Cause
- The loop ran past the end of the array
- Fix
- i < items.length, or for...of when you don't need the index
The program hangs, output never stops
- Cause
- The loop condition never becomes false
- Fix
- Ctrl+C; check that the condition variable changes
The wrong if branch runs
- Cause
- Thresholds are checked in the wrong order
- Fix
- Put stricter conditions first
A condition is always true
- Cause
- Assignment = instead of comparison === in the condition
- Fix
- Compare with ===; MDN advises against assignments in conditions
0 or an empty string got replaced by the default
- Cause
- || fires on any falsy value
- Fix
- Use ??
Exercises and next steps
The code in this lesson already repeats itself: checking the bill, calculating the tip, formatting the time. The next step in the track is functions: how to give such code a name, pass it parameters and get a result back. Algorithm speed, meaning why one loop is instant while a nested one slows down on big data, is covered in the guide to algorithms and complexity.
How this was verified4 · passedchecked 11 October 2026
“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.