JavaScript Fundamentals: Control Flow
Control flow is one of the core JavaScript fundamentals that determines the order in which your code executes. Without it, JavaScript would run line by line from top to bottom. With control flow, you can make decisions, repeat actions, and handle different conditions. This tutorial covers everything you need to direct the execution path of your programs.
Conditional Statements
The if Statement
The if statement executes a block of code only when a specified condition evaluates to true.
const temperature = 25;
if (temperature > 20) {
console.log("It's a warm day!");
}
// Output: It's a warm day!
The condition inside the parentheses must evaluate to a truthy or falsy value. In JavaScript, false, 0, "" (empty string), null, undefined, and NaN are falsy. Everything else is truthy.
A single if checks one condition, but real programs often need to chain multiple conditions. else if lets you test a series of conditions in order, falling through to a final else when none of them match. This is how grading systems, validation rules, and routing logic typically work.
if…else and else if
Use else to execute code when the condition is false, and else if to test multiple conditions.
const score = 75;
let grade;
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else if (score >= 70) {
grade = 'C';
} else {
grade = 'F';
}
console.log(`You got a ${grade}`);
// Output: You got a C
JavaScript evaluates conditions from top to bottom, stopping at the first truthy condition. The order of else if branches matters. If you put the most general condition first, the specific ones after it will never run.
When the logic is short enough to fit on one line, the ternary operator collapses an entire if...else into a single expression.
For simple conditions, the ternary operator offers a compact alternative:
const age = 20;
const status = age >= 18 ? 'adult' : 'minor';
console.log(status);
// Output: adult
Use ternaries for simple assignments, but prefer if...else for complex logic to maintain readability. Nested ternaries quickly become unreadable and are a common source of bugs in code reviews.
When you have a single value tested against many fixed possibilities, switch is often clearer than a long chain of else if branches. Each case acts like an if check, and break prevents the execution from falling through to the next one.
const day = 'Monday';
switch (day) {
case 'Monday':
case 'Tuesday':
case 'Wednesday':
case 'Thursday':
case 'Friday':
console.log('Weekday');
break;
case 'Saturday':
case 'Sunday':
console.log('Weekend');
break;
default:
console.log('Invalid day');
}
// Output: Weekday
Key points about switch:
- The
breakkeyword stops execution and exits the switch - Without
break, execution “falls through” to the next case (likeMondaythroughFridayabove) - The
defaultcase runs when no other case matches - Comparison uses strict equality (
===)
Conditions decide which branch to take, but loops are how JavaScript repeats work. The for loop is the workhorse: you control exactly how many times it runs, which variable tracks progress, and how that variable changes on each pass.
The for Loop
The for loop repeats code a specific number of times:
for (let i = 0; i < 5; i++) {
console.log(`Iteration ${i}`);
}
// Output:
// Iteration 0
// Iteration 1
// Iteration 2
// Iteration 3
// Iteration 4
The for loop is a precise tool: you know the start, stop, and step before it begins. A while loop is simpler. It only needs a condition and keeps going until that condition flips to false, which makes it a natural fit for scenarios where you cannot predict how many iterations are needed, like reading lines from a file or polling for a network response.
The while loop repeats as long as a condition is true:
let count = 0;
while (count < 3) {
console.log(count);
count++;
}
// Output:
// 0
// 1
// 2
Be careful with while loops: if the condition never becomes false, you’ll create an infinite loop. A do...while loop gives you one free pass: the body runs at least once before the condition is checked. This is useful when the check depends on something produced inside the loop body, like user input or a calculation that happens on the first iteration.
The do…while Loop
This variant always executes at least once, because the condition is checked after the code runs:
let num = 0;
do {
console.log(num);
num++;
} while (num < 3);
// Output:
// 0
// 1
// 2
do...while is the least common loop in JavaScript. More often, you need to walk through a collection, and for...of is the cleanest tool for that job. It works on any iterable — arrays, strings, Maps, Sets, and even generators — without requiring you to manage an index variable.
const fruits = ['apple', 'banana', 'cherry'];
for (const fruit of fruits) {
console.log(fruit);
}
// Output:
// apple
// banana
// cherry
This is the cleanest way to loop through arrays when you don’t need the index. When you are looping over an object rather than an array, for...in gives you the keys. Unlike for...of, which works on iterables, for...in enumerates any object’s own and inherited string-keyed properties.
for…in Loop
The for...in loop iterates over enumerable properties of an object:
const person = {
name: 'Alice',
age: 30,
city: 'London'
};
for (const key in person) {
console.log(`${key}: ${person[key]}`);
}
// Output:
// name: Alice
// age: 30
// city: London
All the loop types covered so far run for a fixed number of iterations or until a condition flips. Sometimes you need to exit early or skip ahead. break and continue give you that control, and they work the same way in every loop type.
break
The break statement immediately exits the nearest loop. It is useful when you have found what you need and want to stop searching early, or when an error condition makes continuing pointless.
for (let i = 0; i < 10; i++) {
if (i === 5) {
break;
}
console.log(i);
}
// Output: 0 1 2 3 4
continue does the opposite. Instead of quitting the loop entirely, it skips ahead to the next iteration. A common pattern is to use continue to filter out values you do not want to process before reaching the main loop body.
for (let i = 0; i < 5; i++) {
if (i === 2) {
continue;
}
console.log(i);
}
// Output: 0 1 3 4
Practical example: FizzBuzz
Now that you have seen conditionals, loops, and loop control separately, FizzBuzz pulls them all together into one small program. The problem is simple — print numbers 1 to 15, but replace multiples of 3 with “Fizz,” multiples of 5 with “Buzz,” and multiples of both with “FizzBuzz,” yet the solution demonstrates every control flow concept covered so far in a single, testable example.
Here’s a classic programming challenge that combines everything you’ve learned:
for (let i = 1; i <= 15; i++) {
if (i % 3 === 0 && i % 5 === 0) {
console.log('FizzBuzz');
} else if (i % 3 === 0) {
console.log('Fizz');
} else if (i % 5 === 0) {
console.log('Buzz');
} else {
console.log(i);
}
}
// Output:
// 1
// 2
// Fizz
// 4
// Buzz
// Fizz
// 7
// 8
// Fizz
// Buzz
// 11
// Fizz
// 13
// 14
// FizzBuzz
This example demonstrates how control flow lets you handle multiple conditions and create interesting patterns. The key detail is the order of checks: the compound condition i % 3 === 0 && i % 5 === 0 must come first, because any number divisible by both would match the single % 3 or % 5 checks and never reach the combined case. This ordering principle applies to every conditional chain; most specific first.
Summary
Control flow is fundamental to programming in JavaScript. You now understand:
- Conditional statements:
if,else if,else, and the ternary operator for making decisions - Switch statements: Clean syntax for comparing one value against many cases
- Loops:
for,while,do...while,for...of, andfor...infor repeating code - Loop control:
breakto exit loops early andcontinueto skip iterations
These concepts form the backbone of JavaScript logic. Practice with real-world problems like the FizzBuzz challenge to solidify your understanding.
Guard clauses and nesting
One of the easiest ways to improve control flow is to reduce nesting. A guard clause handles an early exit before the main logic starts, which keeps the rest of the function flatter and easier to read. This is especially helpful when the invalid case is simple and the valid case contains most of the work.
Deeply nested conditionals often signal that the function is doing too much. If you can split the work into smaller steps, each branch becomes easier to follow. That does not just help style. It also makes the code less fragile because each section has a clearer job and fewer hidden assumptions.
Refactoring repeated branches
When you see the same condition in several places, look for a shared decision point. Repeating the same check can make code longer without making it clearer. In many cases, a helper function, a switch statement, or an early return can simplify the branch structure and make the overall flow easier to scan.
This is where readability and correctness come together. A cleaner branch tree is easier to test because it exposes the important paths more clearly. If you can describe each path in a sentence, the code is usually headed in the right direction.
Reading flow aloud
One practical way to judge control flow is to read it as if you were explaining it to a teammate. Start with the condition, then say what happens when it is true and what happens when it is false. If that explanation takes several side notes, the code may need another pass. Shorter branches are not only easier to read, they also make it easier to spot missing cases and accidental repetition. That habit is useful in small scripts and larger applications alike.
Practice Ideas
The best way to learn control flow is to use it in small exercises that feel real. Try writing a function that validates form input, a function that chooses a shipping cost, or a loop that processes a list of items with a stop condition. Each of those problems uses the same building blocks in a different way.
It is also useful to rewrite a messy example after you finish it. Start with a version that works, then look for places where a guard clause, a switch statement, or a different loop type would make the path easier to follow. That kind of review builds good habits much faster than memorizing syntax alone.
Writing flow for future readers
Control flow is often easier to understand when the code reads in the same order the work happens. A clear condition, a direct branch, and a short loop body make the path obvious without extra comments. Future readers benefit when the code tells its own story and does not rely on them remembering how the condition started.
If you are unsure whether a branch is too complex, try explaining it in plain language. If the explanation sounds tangled, the code probably is too. Refactoring the flow into smaller steps usually brings the logic back into focus and makes the program easier to change later.