How to buildOctober 11, 20267 min read
JavaScript from scratchStep 3 of 9

Conditions and loops

In short

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.

TutorialLevel: BeginnerConditions & LoopsPractice: ~45–90 min

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

Verified with

Node.js 24 LTS (24.18.0)Microsoft Edge 155

Checked against docs: October 11, 2026

Contents
  1. 01Logical expressions and truthy/falsy
  2. 02if, else if, else and the ternary operator
  3. 03switch, and when a lookup object is better
  4. 04for, for...of and while loops
  5. 05break, continue and the exit condition
  6. 06Practice: FizzBuzz and an order total
  7. 07Common mistakes: off-by-one and infinite loops
  8. 08Exercises and next steps

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.

truthy.js
// 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);  // → false
Boolean(value) shows how a value will behave in a condition.

if, 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.

if-else.js
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 ₸
23,500 ₸ falls short of 50,000 but passes the 20,000 threshold, so the second branch runs.

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.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} ₸`);
}
The Node.js version: process.argv and process.exitCode exist only there.
terminal
$ 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 12500
Three runs: the default percentage, zero percent and a run without the bill.

Why ?? 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.

or-vs-nullish.js
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);                // → 0
The difference only shows on falsy values: 0 and the empty string.

switch, 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.

switch.js
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 status
The switch version and the object version give the same result.

switch

  • 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.

loops.js
// 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: 4
The // → lines after a loop are what it prints, in order.

for (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.

break-continue.js
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: Latte
continue skips drinks with zero stock; break stops the search at the first match.

The 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:

infinite.js
let i = 0;
while (i < 3) {
  console.log(i);
  // forgot i++ — the condition never becomes false
}
Don't run this: the program prints 0 forever. If you did run it, stop it in the terminal with Ctrl+C.

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”.

fizzbuzz.js
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
// → FizzBuzz
The check for 15 comes first; otherwise the Fizz branch would fire for 15 and FizzBuzz would never be reached.

Now 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 ₸.

order-total.js
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 ₸
The loop builds the subtotal; conditions work out the discount and delivery.

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.

off-by-one.js
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);
A file with a bug.
terminal
$ 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.0
The file path is shortened and stack lines are omitted.

Cannot 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.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.