Skip to content
C Codeloom
JavaScript

Conditionals and Loops in JavaScript

A complete beginner's guide to control flow in JavaScript — if/else, switch, for, while, for...of, break and continue, and the loops you should reach for first.

·10 min read · By Yash Kesharwani
Beginner 11 min read

What you'll learn

  • if, else if, else and when to choose a ternary instead
  • The switch statement and when it earns its keep
  • The classic for loop, while, and do...while
  • for...of, for...in, and which one to actually use
  • break, continue, and labelled loops
  • A short tour of the array iteration methods as loops

Prerequisites

Control flow is what turns a list of values into a program. Every interesting piece of software contains some mix of “if this, then that” and “do this for each of those”. JavaScript’s control-flow constructs are small, learnable, and almost universally familiar from other languages.

if, else if, else

The basic conditional:

const age = 17;

if (age >= 18) {
  console.log("adult");
} else if (age >= 13) {
  console.log("teen");
} else {
  console.log("child");
}

A few style notes:

  • The braces { } are optional for single-statement bodies, but always include them. Brace-less ifs are a famous source of bugs.
  • The condition can be any expression — JavaScript checks its truthiness.
  • Use === and !==, not == and !=.

A common pattern: early return

Long if/else chains are often clearer as a series of early returns:

function gradeFor(score) {
  if (score >= 90) return "A";
  if (score >= 80) return "B";
  if (score >= 70) return "C";
  if (score >= 60) return "D";
  return "F";
}

The “guard clause” version reads top to bottom, with one outcome per line. Use it whenever you can.

Ternary for value-producing conditionals

For something that picks between two values, the ternary ?: is often cleaner than an if:

const message = isLoggedIn ? "Welcome back" : "Please sign in";

Keep ternaries short. If the branches are getting long or nested, use if instead.

switch

switch is useful when you’re comparing one value against several possible exact matches:

function describe(day) {
  switch (day) {
    case "Sat":
    case "Sun":
      return "Weekend";
    case "Mon":
    case "Tue":
    case "Wed":
    case "Thu":
    case "Fri":
      return "Weekday";
    default:
      return "Unknown";
  }
}

console.log(describe("Sat"));   // 'Weekend'
console.log(describe("Wed"));   // 'Weekday'

Three things to know:

  • switch uses === (strict equality) for matching.
  • Without break or return, control falls through to the next case. The “Weekend” case above intentionally lets "Sat" fall through to "Sun"’s body.
  • default runs if no case matches. It can appear anywhere in the block, but the end is conventional.

switch shines when there are many cases. For a small number of comparisons, an object lookup or an if chain is often clearer:

// Often cleaner than a switch
const grades = { A: 4.0, B: 3.0, C: 2.0, D: 1.0, F: 0.0 };
console.log(grades["B"] ?? 0);   // 3.0

The while loop

Run a block while a condition is truthy:

let n = 1;
while (n <= 5) {
  console.log(n);
  n++;
}
// 1 2 3 4 5

Useful when you don’t know upfront how many iterations you need:

let guess = 0;
const target = Math.floor(Math.random() * 100);
while (guess !== target) {
  guess = Math.floor(Math.random() * 100);
}

Always make sure your condition will eventually become false, or you’ll have an infinite loop. (Ctrl + C cancels a runaway Node process.)

The do…while loop

A variant that runs the body at least once before checking:

let input;
do {
  input = prompt("Enter 'q' to quit");
} while (input !== "q");

Use it when the loop body produces the value the condition examines — input prompts, retries, the “act, then check” pattern.

The classic for loop

When you know roughly how many iterations and want fine control:

for (let i = 0; i < 5; i++) {
  console.log(i);
}
// 0 1 2 3 4

Three semicolon-separated parts:

  1. Initialiser — runs once before the loop. Often declares a counter with let.
  2. Condition — checked before each iteration; the loop stops when it’s false.
  3. Step — runs after each iteration. i++ is the most common.

The classic for is invaluable when you need to iterate backwards, in steps of two, or stop mid-array:

const a = [10, 20, 30, 40, 50];

for (let i = a.length - 1; i >= 0; i--) {
  console.log(a[i]);
}

for (let i = 0; i < a.length; i += 2) {
  console.log(a[i]);   // every other element
}

for…of — the modern default

For iterating any iterable (arrays, strings, Maps, Sets, generators), for...of is the cleanest choice:

const fruits = ["apple", "banana", "cherry"];
for (const fruit of fruits) {
  console.log(fruit);
}

for (const ch of "hi") {
  console.log(ch);
}
// 'h' 'i'

When you need the index, ask the array for its entries():

for (const [i, fruit] of fruits.entries()) {
  console.log(i, fruit);
}

This is the loop you should reach for first when iterating an array.

for…in — for object keys

for...in iterates the keys of an object (and inherited keys, which is occasionally surprising):

const user = { name: "Ada", age: 36 };
for (const key in user) {
  console.log(key, user[key]);
}

For arrays, don’t use for...in — it iterates string indices and inherited properties. Use for...of (or forEach) for arrays, for...of with Object.entries for objects.

The cleaner modern pattern for objects, repeated from the objects post:

for (const [key, value] of Object.entries(user)) {
  console.log(`${key}: ${value}`);
}

break and continue

Both work in any loop.

break exits the loop immediately:

for (const n of [3, 7, 2, 9, 5]) {
  if (n > 8) {
    console.log("found one over 8:", n);
    break;
  }
}

continue skips the rest of the current iteration and moves on:

for (let i = 1; i <= 10; i++) {
  if (i % 2 === 0) continue;
  console.log(i);
}
// 1 3 5 7 9

A note: you can’t break or continue out of forEach, map, or filter. They run their callback to completion. If you need that, use for...of.

Labelled loops (rarely needed)

To break out of a nested loop, you can label the outer one:

outer: for (let i = 0; i < 5; i++) {
  for (let j = 0; j < 5; j++) {
    if (i * j > 6) {
      console.log("done at", i, j);
      break outer;
    }
  }
}

This works but usually signals the loop should be refactored into a function with an early return.

Try it yourself. Print the multiplication table from 1 to 5 in a 5×5 grid. Use two nested for loops and template literals. Hint: String(value).padStart(3) makes the columns line up.

Array methods as loops

For the most common cases — transform, filter, sum — JavaScript’s array methods are usually clearer than a manual loop. We covered them in the arrays post:

const nums = [1, 2, 3, 4, 5];

const doubled = nums.map((n) => n * 2);
const evens   = nums.filter((n) => n % 2 === 0);
const sum     = nums.reduce((s, n) => s + n, 0);

These are loops, just spelled differently. They each return a new array (or value) and never mutate the original. Reach for them when the work fits — fall back to for...of when it doesn’t.

A simple rule of thumb:

  • Transforming every item? map.
  • Keeping some items? filter.
  • Combining into one value? reduce.
  • Doing something with each item but not building a new collection? for...of.
  • Need to break early or use await? for...of.

Common pitfalls

A handful of beginner bugs worth flagging.

1. Reassignment in a condition

let x = 5;
if (x = 10) {   // assignment, not comparison
  console.log("always runs");
}

ESLint catches this. Use === instead.

2. Forgetting break in switch

Without break, every case below the matched one runs too. Sometimes that’s deliberate (the weekend example above), but usually it’s a bug. Always include break (or return) unless you specifically want fallthrough.

3. for...of on plain objects

for...of doesn’t work on plain objects directly — they aren’t iterable. Convert with Object.entries:

const user = { name: "Ada", age: 36 };

// Doesn't work
// for (const x of user) { ... }   // TypeError

// Works
for (const [key, value] of Object.entries(user)) {
  console.log(key, value);
}

4. Mutating an array while iterating it

const a = [1, 2, 3, 4, 5];
for (let i = 0; i < a.length; i++) {
  if (a[i] % 2 === 0) a.splice(i, 1);   // shifts indices — skips items
}

Easier and bug-free: produce a new array.

const odds = a.filter((n) => n % 2 !== 0);

Try it yourself. Write fizzBuzz(n) that prints the numbers 1 through n, but replaces multiples of 3 with "Fizz", multiples of 5 with "Buzz", and multiples of both with "FizzBuzz". Use a for loop, if/else if, and template literals.

A small worked example

A program that processes a roster, separates passing and failing students, and prints a formatted report.

const students = [
  { name: "Alice",  score: 82 },
  { name: "Bob",    score: 47 },
  { name: "Carol",  score: 91 },
  { name: "Dave",   score: 56 },
  { name: "Eve",    score: 73 },
];

function report(students, passingScore = 60) {
  const passing = [];
  const failing = [];

  for (const { name, score } of students) {
    const bucket = score >= passingScore ? passing : failing;
    bucket.push({ name, score });
  }

  const avg =
    students.reduce((s, st) => s + st.score, 0) / students.length;

  console.log(`Class average: ${avg.toFixed(1)}`);
  console.log(`Passing (${passing.length}):`);
  for (const { name, score } of passing) {
    console.log(`  ${name.padEnd(8)} ${score}`);
  }
  console.log(`Failing (${failing.length}):`);
  for (const { name, score } of failing) {
    console.log(`  ${name.padEnd(8)} ${score}`);
  }
}

report(students);

Output:

Class average: 69.8
Passing (3):
  Alice    82
  Carol    91
  Eve      73
Failing (2):
  Bob      47
  Dave     56

Destructuring in for...of, a ternary to pick a bucket, reduce for the average, template literals and padEnd for the formatting — every concept in the last few posts comes together here.

Recap

You now know:

  • if/else if/else and the early-return pattern that often replaces them
  • The ternary ?: for compact value-producing conditionals
  • switch and its case/default/fallthrough semantics
  • while, do...while, the classic for, and when each fits
  • for...of for iterables (the modern default) and for...in for object keys
  • break and continue for early exits
  • map, filter, reduce as cleaner loops for the common cases
  • The pitfalls — accidental assignment, missing breaks, mutating during iteration

Where to go next

You have now covered the complete fundamentals of modern JavaScript — every value, every operator, every loop, every container. From here, three good directions:

  1. Functions in depth — parameters, arrow vs. regular, closures, callbacks. The next series picks up here.
  2. The DOM — using JavaScript in the browser to read and modify the page.
  3. A small project — pick something you wanted to build from the first post and start. Look up what you don’t know as you go.

The single best thing you can do now is write code. Every concept you’ve read about will only stick once you’ve used it for something you actually cared about.

Questions or feedback? Email codeloomdevv@gmail.com.