Control flow decides which branch your program takes. JavaScript offers the familiar if, else if, else, the switch statement, the ternary expression, and the short-circuit operators that double as control structures. This lesson walks through each form with real examples.
if, else if, else
The if statement tests an expression in parentheses. If the value is truthy, the block in braces runs. Chain else if for additional branches and end with else for the default. Always use braces even for one-line bodies to avoid bugs when you later add a second line.
const score = 87;
let grade;
if (score >= 90) {
grade = "A";
else if (score >= 80) {
grade = "B";
else if (score >= 70) {
grade = "C";
else if (score >= 60) {
grade = "D";
else {
grade = "F";
}
console.log(`Score ${score} is a ${grade}`); // Score 87 is a B
Truthy and falsy values
JavaScript will coerce any value to a boolean when used in an if. The falsy values are false, 0, -0, 0n, the empty string, null, the missing-value primitive, and NaN. Everything else is truthy, including the string "0" and the empty object {}.
function describe(value) {
if (value) {
return "truthy";
}
return "falsy";
}
console.log(describe(0)); // falsy
console.log(describe("")); // falsy
console.log(describe(null)); // falsy
console.log(describe(NaN)); // falsy
console.log(describe("hello")); // truthy
console.log(describe([])); // truthy (empty array is truthy!)
console.log(describe({})); // truthy
The switch statement
switch compares one value against many cases with strict equality. Each case ends with break to prevent fallthrough. The default case is the catch-all and is optional. Group multiple labels when one body should handle several values.
function httpStatus(code) {
let label;
switch (code) {
case 200:
label = "OK";
break;
case 301:
case 302:
label = "Redirect";
break;
case 404:
label = "Not Found";
break;
case 500:
label = "Server Error";
break;
default:
label = "Unknown";
}
return label;
}
console.log(httpStatus(404)); // Not Found
The ternary and short-circuit operators
The ternary condition ? a : b is a one-line if/else that returns a value. The && operator short-circuits and returns the first falsy operand or the last value. The || operator returns the first truthy operand. The nullish coalescing operator ?? returns the right side only if the left is null or the missing-value primitive.
const age = 19;
const status = age >= 18 ? "adult" : "minor";
let a = null;
let b;
const c = 0;
const d = "fallback";
console.log(a ?? d); // fallback
console.log(b ?? d); // fallback
console.log(c ?? d); // 0 (nullish coalescing only treats null and the missing-value primitive as missing)
console.log(c || d); // fallback (logical OR treats 0 as missing)
// Short-circuit default object
const options = userInput || { debug: false };
Optional chaining for safe property access
The ?. operator stops evaluating and returns the missing-value primitive if the left side is null or the missing-value primitive. Use it to safely traverse deep object paths without a long chain of && checks.
const user = { profile: { settings: { theme: "dark" } } };
console.log(user?.profile?.settings?.theme); // dark
console.log(user?.profile?.billing?.plan); // returns the missing-value primitive
// Optional chaining for method calls and array access
const first = items?.[0];
const result = obj.method?.();
Summary
Use if/else for branching, switch for one value against many cases, ternary for short inline choices, and ?? and ?. for safe access. The next lesson covers loops: for, for-of, for-in, while, and the methods like forEach, map, and filter that show up in every JavaScript program.
Common pitfalls in control flow
The most common bug in control flow is forgetting break inside a switch case, which causes fallthrough into the next case. The next most common is treating an empty array or zero as falsy when the intent was to test presence rather than truthiness. Always prefer explicit comparisons (length, ===, ?? ) over implicit truthiness checks when the distinction matters. The third common bug is a deeply nested ternary chain that is impossible to read. Use an if/else block, a switch, or a lookup table instead.
// Avoid this
const result = a ? "A" : b ? "B" : c ? "C" : "D";
// Prefer this
function pick(a, b, c) {
if (a) return "A";
if (b) return "B";
if (c) return "C";
return "D";
}
// Or a lookup
const table = { x: "A", y: "B", z: "C" };
const result2 = table[key] ?? "D";
Performance and best practices
Modern engines optimize all control-flow forms well. The choice between if/else, switch, and a lookup table should be driven by readability, not micro-performance. Switch with many cases can be compiled to a jump table and is faster than a long if/else chain, but the difference is rarely noticeable. Use early returns to flatten nested conditionals: each guard clause that returns early reduces the indentation of the remaining code and makes the happy path easy to follow.
function processOrder(order) {
if (!order) return null; // guard
if (!order.items?.length) return null; // guard
if (order.status !== "paid") return null; // guard
// happy path, no indentation
return order.items.map(shipItem);
}
Comparison with Python
Python's if/elif/else mirrors JavaScript's if/else if/else. Python's match statement, added in 3.10, plays the same role as JavaScript's switch but with pattern matching that supports destructuring, type checks, and guards. Python's ternary is a ? b : c if x else y, written as x if condition else y. Python's and, or, and the walrus operator := cover the same ground as &&, ||, and the new assignment expressions in JavaScript. The nullish coalescing operator ?? exists in many of them now. The biggest practical difference is that Python uses truthiness rules that include empty collections, so [] is falsy in Python but truthy in JavaScript.