Skip to main content
JavaScript beginner Lesson 6 of 24

Control Flow in JavaScript

Learn how to direct your program's execution with if/else, switch, loops (for, while, for...of, for...in), break, continue, and labeled statements.

Conditional Statements

Control flow is what makes programs useful: instead of running the same sequence of statements every time, your code can make decisions, repeat work, and skip steps based on conditions. Every meaningful program — from a login check to a data pipeline — relies on control flow to behave differently in different situations.

if / else if / else

The if statement is the most fundamental control structure. It evaluates a condition and runs a block of code only when that condition is truthy. Chaining else if handles multiple branches, and a final else catches everything that didn’t match. The condition is coerced to a boolean — any truthy value takes the if branch.

const score = 74;

if (score >= 90) {
  console.log("Grade: A");
} else if (score >= 80) {
  console.log("Grade: B");
} else if (score >= 70) {
  console.log("Grade: C");
} else {
  console.log("Grade: F");
}
// "Grade: C"

Always use braces {} even for single-line bodies — omitting them is a common source of subtle bugs when adding a second statement later.

Truthy and falsy in conditions

Because JavaScript coerces any value to boolean in a condition, you can write concise guards without explicit comparisons. The key is knowing which values are falsy — 0, "", null, undefined, NaN, and false — and which aren’t. Empty arrays and empty objects are truthy, which surprises many beginners.

const username = "";

// "" is falsy, so this triggers
if (!username) {
  console.log("Please enter a username");
}

// Careful: empty array and empty object are truthy
const items = [];
if (items) {
  console.log("This runs — [] is truthy");
}
if (items.length > 0) {
  console.log("This doesn't run — length is 0");
}

switch Statement

switch is useful when you need to compare a single value against many possible cases. It compares using strict equality (===), so there’s no type coercion. Always include break at the end of each case — without it, execution “falls through” into the next case, which is one of the most common sources of bugs in JavaScript.

const day = "Monday";

switch (day) {
  case "Saturday":
  case "Sunday":
    console.log("Weekend");
    break;
  case "Monday":
  case "Tuesday":
  case "Wednesday":
  case "Thursday":
  case "Friday":
    console.log("Weekday");
    break;
  default:
    console.log("Unknown day");
}
// "Weekday"

Intentional fallthrough

Sometimes fallthrough is exactly what you want — when multiple cases should share cumulative behavior. When you do this deliberately, always add a comment so reviewers know it wasn’t an oversight.

// Accumulating permissions by role — fallthrough is deliberate here
switch (role) {
  case "admin":
    permissions.push("delete");
    // falls through
  case "editor":
    permissions.push("write");
    // falls through
  case "viewer":
    permissions.push("read");
    break;
  default:
    permissions.push("none");
}

The switch pitfall: no strict type check on the expression

const input = "1";

switch (input) {
  case 1:    // number 1 — won't match "1"
    console.log("number one");
    break;
  case "1":  // string "1" — this runs
    console.log("string one");
    break;
}

Ternary Operator

For simple inline conditionals (covered in detail in the operators tutorial):

const label  = user.isAdmin ? "Admin" : "User";
const plural = count === 1 ? "item" : "items";

for Loop

The classic for loop gives you full control over iteration: an initializer, a condition checked before each step, and an update expression run after each step. It’s best when you know the number of iterations upfront or when you need the numeric index for something other than just accessing elements.

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

// Counting backwards
for (let i = 10; i > 0; i -= 2) {
  console.log(i); // 10 8 6 4 2
}

// Iterating an array by index (useful when you need the index itself)
const fruits = ["apple", "banana", "cherry"];
for (let i = 0; i < fruits.length; i++) {
  console.log(`${i}: ${fruits[i]}`);
}

while Loop

A while loop runs as long as its condition is truthy and is the right choice when you don’t know the number of iterations in advance. Connection retries, polling loops, and processing queues are all natural fits. Always ensure something inside the loop eventually makes the condition false, or the loop runs forever.

let attempts = 0;
const MAX = 3;

while (attempts < MAX) {
  const success = tryConnection(); // hypothetical
  if (success) break;
  attempts++;
}

// Polling until a queue is empty
while (queue.length > 0) {
  const job = queue.shift();
  process(job);
}

Watch out for infinite loops — make sure something inside the loop eventually makes the condition false.

do…while Loop

A do...while loop runs its body first, then checks the condition — guaranteeing at least one execution regardless of whether the condition is true. This is the right tool when the action must happen before you can even evaluate whether to continue, such as prompting a user for input or displaying a menu.

let input;

// The prompt must run at least once before we can check the value
do {
  input = prompt("Enter a number between 1 and 10:");
} while (input < 1 || input > 10);

// Also useful for menu-driven programs
let choice;
do {
  displayMenu();
  choice = getChoice();
  handleChoice(choice);
} while (choice !== "quit");

for…of — Iterating Values

for...of is the modern, idiomatic way to iterate over any collection. It works on arrays, strings, Maps, Sets, generators, and anything that implements the iterator protocol — without the footguns of for...in on arrays. When you just need each element, prefer for...of over a classic for loop.

const colors = ["red", "green", "blue"];

for (const color of colors) {
  console.log(color); // "red", "green", "blue"
}

// Works on strings — iterates individual characters
for (const char of "hello") {
  console.log(char); // "h", "e", "l", "l", "o"
}

// With index via entries() — best of both worlds
for (const [index, color] of colors.entries()) {
  console.log(`${index}: ${color}`);
}

// Iterating a Map gives [key, value] pairs
const userRoles = new Map([["Alice", "admin"], ["Bob", "editor"]]);
for (const [user, role] of userRoles) {
  console.log(`${user} is ${role}`);
}

for…in — Iterating Keys

for...in iterates over the enumerable string property keys of an object. It’s designed specifically for plain objects when you need to loop over their keys dynamically. Avoid using it on arrays — it iterates index strings, not numeric indices, and also picks up any inherited properties added to Array.prototype.

const config = {
  host: "localhost",
  port: 5432,
  database: "mydb"
};

for (const key in config) {
  console.log(`${key}: ${config[key]}`);
}
// host: localhost
// port: 5432
// database: mydb

Why not to use for…in on arrays

const arr = ["a", "b", "c"];

for (const key in arr) {
  console.log(key);         // "0", "1", "2"  — string keys, not numbers
  console.log(typeof key);  // "string"
}

// It also iterates inherited properties if someone extended Array.prototype.
// Use for...of or forEach for arrays instead.

for…of vs for…in — Quick Reference

for…offor…in
IteratesValuesKeys (strings)
Works onArrays, strings, Map, Set, iterablesPlain objects, arrays (avoid)
Array indexNo (use .entries())Yes, but as string
Inherited propsNoYes (use hasOwn to filter)

break and continue

break and continue give you fine-grained control inside a loop. break exits the loop entirely — useful when you’ve found what you need and further iterations would be wasted work. continue skips the rest of the current iteration and jumps straight to the next one — useful for filtering out unwanted items without nesting an if around the whole body.

// break — stop as soon as you find the target
const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 3, name: "Charlie" }
];

let found = null;
for (const user of users) {
  if (user.id === 2) {
    found = user;
    break; // no point iterating further
  }
}

// continue — skip negatives, collect positives
const numbers = [1, -2, 3, -4, 5];
const positives = [];

for (const n of numbers) {
  if (n < 0) continue; // skip this iteration
  positives.push(n);
}
// [1, 3, 5]

Labeled Statements

Labels let you break out of or continue a specific outer loop when dealing with nested loops — something a plain break can’t do. They’re rarely needed in practice, and if you find yourself reaching for them regularly, it’s usually a sign that the nested logic should be extracted into its own function.

// Without labels, break only exits the innermost loop
outer: for (let i = 0; i < 3; i++) {
  for (let j = 0; j < 3; j++) {
    if (i === 1 && j === 1) {
      break outer; // exits both loops at once
    }
    console.log(`${i},${j}`);
  }
}
// 0,0  0,1  0,2  1,0  — stops before 1,1

Combining Control Flow — A Real Example

Real programs combine these structures together. The early-return pattern, continue for skipping invalid data, and a final conditional are all common idioms that keep the happy path readable without deep nesting.

// Process an order: validate, apply discount, check stock
function processOrder(order) {
  // Early return pattern — handle errors first, keep the main path clean
  if (!order) return { error: "No order provided" };
  if (!order.items?.length) return { error: "Order has no items" };

  let total = 0;
  const unavailable = [];

  for (const item of order.items) {
    if (item.quantity <= 0) continue; // skip invalid quantities

    const stock = getStock(item.productId);
    if (stock < item.quantity) {
      unavailable.push(item.productId);
      continue; // can't fulfill this item, track it and move on
    }

    total += item.price * item.quantity;
  }

  if (unavailable.length > 0) {
    return { error: `Out of stock: ${unavailable.join(", ")}` };
  }

  // Apply a 10% discount on orders over $100
  const discount = total > 100 ? 0.1 : 0;
  return { total: total * (1 - discount), discount };
}

This pattern — validate early, loop with continue to skip bad data, return a result — is idiomatic in production JavaScript.

What’s Next

With control flow covered, you have the tools to write programs that make decisions and repeat tasks. The next tutorials cover functions — where you package this logic for reuse.

Frequently Asked Questions

What is the difference between for...of and for...in?
for...of iterates over the values of any iterable (arrays, strings, Maps, Sets). for...in iterates over the enumerable property keys of an object. Using for...in on an array is a common mistake — it iterates over index strings like '0', '1', and also inherited properties. Use for...of for arrays.
Do I always need a break in a switch statement?
Yes, unless you intentionally want fallthrough. Without break, execution continues into the next case — a very common source of bugs. Some teams use ESLint's no-fallthrough rule to catch this automatically.
When should I use while instead of for?
Use while when you don't know how many iterations you need upfront — polling until a condition is met, reading lines until EOF, or retrying until success. Use for when you know the iteration count or are looping over a collection.