Chapter 6: Control Flow
Control flow determines the order in which a program’s statements are executed. By default, JavaScript runs code from top to bottom, one statement after another. Control structures interrupt or redirect that flow, allowing programs to make decisions, repeat actions, or exit early when conditions are met.
These constructs (such as if, switch, and looping statements) form the backbone of logical structure in JavaScript. They allow you to describe not just what your code should do, but under what circumstances and how many times it should do it.
In this chapter, you will explore JavaScript’s main flow-control tools: conditional branching with if and switch, iterative looping with while and for variations, and fine-grained loop control using break, continue, and labels. You will also examine the conditional (ternary) operator for concise decision-making within expressions.
Together, these elements let your programs respond dynamically to data, automate repetitive actions, and maintain readable, predictable logic; hallmarks of effective JavaScript code.
if, else if, else
The if statement is the foundation of decision-making in JavaScript. It evaluates a condition and executes a block of code only if that condition is true. Optional else if and else clauses allow you to test multiple conditions in sequence and define fallback behavior when none of them match.
let temperature = 25;
if (temperature > 30) {
console.log("It’s hot today.");
} else if (temperature >= 20) {
console.log("The weather is mild.");
} else {
console.log("It’s cool outside.");
}
Each condition is tested in order, and only the first true condition runs its corresponding block. If no conditions evaluate to true, the else block (if present) executes by default.
if (loggedIn)
console.log("Welcome back!");
Although this works, always prefer the explicit, brace-enclosed form:
if (loggedIn) {
console.log("Welcome back!");
}
Nested if statements
When multiple related conditions depend on different levels of logic, if statements can be nested. Each block can contain further decision points, allowing fine-grained control of behavior.
let user = { name: "Robin", role: "admin" };
if (user) {
if (user.role === "admin") {
console.log("Access granted.");
} else {
console.log("Access limited.");
}
}
if statements can quickly become difficult to read. When logic grows complex, consider using switch statements or returning early from functions to simplify control paths.
Falsy conditions
JavaScript treats certain values as falsy. These include false, 0, "", null, undefined, and NaN. Any falsy value causes an if condition to fail, while all other values are treated as truthy.
let name = "";
if (name) {
console.log("Name provided.");
} else {
console.log("No name given.");
}
This behavior allows concise tests but can also introduce subtle bugs if falsy values like 0 or "" are legitimate inputs.
if (value !== null && value !== undefined)) instead of relying on truthiness.
The if family of statements remains the most direct way to guide JavaScript’s flow. Clear, well-structured conditions are key to making intent obvious and maintaining logical control throughout your programs.
switch with fallthrough control
The switch statement provides a clean way to handle multiple possible values of a single expression. It compares the result of an expression against a list of case labels and executes the matching block. A default label handles any unmatched cases.
let color = "green";
switch (color) {
case "red":
console.log("Stop");
break;
case "yellow":
console.log("Get ready");
break;
case "green":
console.log("Go");
break;
default:
console.log("Unknown color");
}
Each case is checked in order, and execution stops when a break statement is encountered. If break is omitted, JavaScript continues executing the following cases until it finds one with break or the switch block ends. This is known as fallthrough.
Using fallthrough intentionally
Fallthrough can be useful when multiple cases should execute the same code. Instead of duplicating logic, you can stack case labels together.
let day = "Saturday";
switch (day) {
case "Saturday":
case "Sunday":
console.log("Weekend");
break;
default:
console.log("Weekday");
}
break after the shared section to prevent unwanted continuation.
Fallthrough hazards
Unintentional fallthrough is a common source of bugs, especially when a break is accidentally omitted. JavaScript’s interpreter will continue executing subsequent cases until a break or return is encountered.
let code = 1;
switch (code) {
case 1:
console.log("One");
case 2:
console.log("Two");
case 3:
console.log("Three");
break;
}
Output:
One
Two
Three
switch cases for missing break statements. Unintended fallthrough can execute multiple blocks and produce confusing output.
Using return in functions
When a switch is used inside a function, you can use return instead of break to exit immediately. This approach is often cleaner and eliminates the risk of fallthrough.
function getMessage(status) {
switch (status) {
case 200:
return "OK";
case 404:
return "Not Found";
case 500:
return "Server Error";
default:
return "Unknown";
}
}
console.log(getMessage(404));
Strict comparison
switch uses strict equality (===) when comparing the target expression with each case label. No type coercion occurs, so types must match exactly.
let value = "1";
switch (value) {
case 1:
console.log("Number one");
break;
case "1":
console.log("String one");
break;
}
This will output String one because "1" === 1 is false.
switch cases to match values flexibly, convert the expression or cases explicitly before comparison, for example using String() or Number().
The switch statement is ideal for matching discrete values, state codes, or user selections. When used with careful break placement, it produces cleaner, more readable logic than long chains of if...else if statements.
while, do while
Loops allow a block of code to run repeatedly while a condition remains true. The while and do while statements are the simplest looping constructs in JavaScript. Both evaluate a condition before (or after) executing their loop body, making them ideal for scenarios where repetition depends on dynamic data.
The while loop
The while loop checks its condition before each iteration. If the condition is true, the loop body executes; if false, the loop ends immediately.
let count = 0;
while (count < 5) {
console.log("Count is " + count);
count++;
}
This loop prints values from 0 to 4. Because the condition is tested before every iteration, it is possible for the body to never run at all if the condition starts as false.
while loop can run indefinitely. Always make sure something inside the loop modifies a value that affects the condition.
The do while loop
The do while loop guarantees that its body executes at least once, even if the condition is initially false. This is because the condition is evaluated after each iteration rather than before.
let value = 0;
do {
console.log("Value is " + value);
value++;
} while (value < 3);
Here, the output will include values 0, 1, and 2. Even if value began greater than 3, the loop body would still execute once before stopping.
do while when you must run the loop body at least once, such as when reading user input or performing an initial setup before validation.
Breaking or skipping within loops
Both while and do while loops support break and continue for finer control. break exits the loop entirely, while continue skips to the next iteration.
let n = 0;
while (n < 10) {
n++;
if (n === 3) continue; // skip this iteration
if (n === 8) break; // stop the loop
console.log(n);
}
This example skips printing 3 and stops altogether when n reaches 8.
break and continue can make logic concise but should be used sparingly. Overuse can obscure loop behavior and make control flow harder to follow.
The while and do while constructs provide simple, condition-driven repetition. They are best suited for loops where the number of iterations is not known in advance and depends entirely on runtime conditions.
for and for...of
The for loop is one of JavaScript’s most versatile looping constructs. It allows you to define initialization, condition, and iteration expressions all in a single statement. This makes it ideal for loops that execute a specific number of times or iterate through a sequence with predictable bounds.
The classic for loop
The traditional for loop includes three parts inside parentheses, separated by semicolons:
for (initialization; condition; final-expression) {
// loop body
}
Each section controls a different part of the loop:
- Initialization: executed once before the loop starts
- Condition: checked before each iteration; if false, the loop ends
- Final expression: executed at the end of each iteration
for (let i = 0; i < 5; i++) {
console.log("Iteration " + i);
}
This outputs values from 0 to 4. When i reaches 5, the condition i < 5 becomes false and the loop stops.
let are block-scoped, meaning they exist only inside the loop. If you use var, the variable will remain accessible after the loop finishes.
The for...of loop
Introduced in ES2015, the for...of loop provides a clean, direct way to iterate over iterable objects such as arrays, strings, maps, and sets. It retrieves each value in sequence without using an index.
const colors = ["red", "green", "blue"];
for (const color of colors) {
console.log(color);
}
This loop prints each element in colors. It works with any object that implements the iterable protocol, which includes most built-in collections.
for...of when you only need element values, not their indices. It is more readable and less error-prone than managing counters manually.
Iterating over strings
Because strings are iterable, for...of can loop through each character directly:
for (const ch of "JavaScript") {
console.log(ch);
}
This is often simpler than using an index-based loop like for (let i = 0; i < str.length; i++).
Comparing for, for...of, and for...in
Although similar in appearance, for...in is different: it iterates over property names of an object, not values. It is useful for objects but should not be used with arrays when element order matters.
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
console.log(key + ": " + obj[key]);
}
for...in only for plain objects or when enumerating property names. For arrays or other collections, prefer for...of to get the actual values.
break, continue, and labels
JavaScript provides several keywords for fine-grained control of loops and blocks. The break and continue statements alter the normal flow of iteration, while labels (used rarely) allow control to move out of specific named blocks or nested loops.
Exiting a loop early
The break statement stops execution of the nearest enclosing loop or switch immediately, jumping to the statement that follows it. It is often used when a certain condition has been met or when continuing further would be unnecessary.
for (let i = 0; i < 10; i++) {
if (i === 5) {
break; // stop when i reaches 5
}
console.log(i);
}
This prints values from 0 through 4, then exits the loop when i equals 5.
break to improve efficiency by halting loops as soon as their purpose is fulfilled, rather than letting unnecessary iterations run.
Skipping an iteration
The continue statement ends the current loop iteration immediately and moves on to the next one. It does not exit the loop completely, just skips the remainder of that iteration’s body.
for (let i = 1; i <= 6; i++) {
if (i % 2 === 0) {
continue; // skip even numbers
}
console.log(i);
}
In this example, only the odd numbers (1, 3, 5) are printed. The continue statement is particularly useful when you need to ignore certain data without breaking the loop entirely.
continue in a way that skips code responsible for updating loop variables.
Labeled statements
JavaScript allows you to assign a label to a statement or block. A label is an identifier followed by a colon. You can then reference that label with break or continue to control which loop or block to affect, which is useful in nested structures.
outerLoop: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === j) {
continue outerLoop; // skip to the next outer loop iteration
}
console.log(i, j);
}
}
This skips any pair where i equals j by continuing the outer loop rather than the inner one. Labels can also be used with break to exit multiple nested loops at once.
search:
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 5; j++) {
if (i * j === 6) {
console.log("Found at", i, j);
break search; // exit both loops
}
}
}
In general, break and continue keep your loops efficient and expressive, while labels provide an escape hatch for complex iteration patterns. Used carefully, these tools help you manage control flow cleanly and precisely.
Conditional operator
The conditional operator (? :), often called the ternary operator, is a compact way to perform simple conditional evaluations within an expression. It offers a concise alternative to an if...else statement, returning one of two values depending on a given condition.
let age = 18;
let status = (age >= 18) ? "Adult" : "Minor";
console.log(status); // "Adult"
The syntax consists of three parts:
condition ? expressionIfTrue : expressionIfFalse
First, the condition is evaluated. If it is truthy, the expression before the colon executes; otherwise, the expression after the colon executes. The entire construct is an expression, meaning it produces a value and can be used anywhere a value is expected—such as in assignments, function arguments, or return statements.
Nested conditional expressions
Conditional operators can be chained or nested to handle multiple outcomes, though this can quickly reduce readability. Each additional condition appears as another ? : sequence inside one of the branches.
let score = 72;
let grade = (score >= 90) ? "A"
: (score >= 75) ? "B"
: (score >= 60) ? "C"
: "D";
console.log(grade); // "C"
if...else if statements for clarity.
Using the operator in expressions
Because the conditional operator returns a value, it can be embedded directly inside other expressions, allowing elegant one-liners when appropriate.
function greet(user) {
return "Hello, " + (user ? user.name : "Guest") + "!";
}
console.log(greet({ name: "Robin" })); // "Hello, Robin!"
console.log(greet(null)); // "Hello, Guest!"
This example chooses between user.name and a default string based on whether user is truthy.
The conditional operator’s ability to condense decision-making into a single line makes it a valuable tool for writing expressive, minimal code, especially when used judiciously and with readability in mind.