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…of | for…in | |
|---|---|---|
| Iterates | Values | Keys (strings) |
| Works on | Arrays, strings, Map, Set, iterables | Plain objects, arrays (avoid) |
| Array index | No (use .entries()) | Yes, but as string |
| Inherited props | No | Yes (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.