Difficulty: Beginner
Control flow is the order in which individual statements, instructions, or function calls are executed or evaluated. By default, JavaScript executes code from top to bottom, one statement at a time. Control flow statements allow you to break out of this linear execution and make decisions, creating programs that respond dynamically to different conditions.
The if/else statement is the most fundamental control flow construct. It evaluates a condition and executes one block of code if the condition is truthy, and optionally another block if it is falsy. You can chain multiple conditions using else if to create a decision tree. Remember that JavaScript checks truthiness, not strict boolean true - so if ("hello"), if (42), and if ([]) all evaluate to true because those values are truthy.
The switch statement is an alternative to long if/else if chains when you need to compare a single value against multiple possibilities. It uses strict equality (===) for comparisons and requires break statements to prevent fall-through (executing subsequent cases after a match). Fall-through is sometimes used intentionally to handle multiple cases with the same code, but forgetting a break is one of the most common bugs. Modern JavaScript developers often prefer object lookups or Maps over switch statements for cleaner code.
The ternary operator (condition ? expressionIfTrue : expressionIfFalse) is a concise alternative to simple if/else blocks. Unlike if/else, the ternary operator is an expression, meaning it produces a value and can be used inside variable assignments, function arguments, and template literals. Avoid nesting ternary operators more than one level deep - while syntactically valid, nested ternaries quickly become unreadable.
Short-circuit evaluation with logical operators provides another way to write conditional logic. The expression condition && doSomething() only executes doSomething() if condition is truthy. The expression value || defaultValue returns defaultValue only if value is falsy. The optional chaining operator (?.) extends this idea by short-circuiting when it encounters null or undefined in a property access chain, preventing the dreaded "Cannot read property of undefined" error. Combined with nullish coalescing (??), optional chaining creates powerful patterns for safely navigating nested data structures.
const score = 85;
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}, Grade: ${grade}`);
// Truthy/falsy in conditions
const name = "";
if (name) {
console.log(`Hello, ${name}`);
} else {
console.log("Name is empty or not provided");
}
// Multiple conditions
const age = 25;
const hasLicense = true;
if (age >= 18 && hasLicense) {
console.log("Can drive");
}
The if/else if chain evaluates conditions top to bottom and executes the first matching block. Conditions are checked for truthiness, so an empty string evaluates to false. You can combine conditions with && (AND) and || (OR) logical operators.
const fruit = "apple";
switch (fruit) {
case "apple":
console.log("Apples are $1.50/lb");
break;
case "banana":
console.log("Bananas are $0.75/lb");
break;
case "orange":
case "tangerine": // Intentional fall-through
console.log("Citrus fruits are $2.00/lb");
break;
default:
console.log("Unknown fruit");
}
// Object lookup alternative (often cleaner)
const prices = {
apple: 1.50,
banana: 0.75,
orange: 2.00,
tangerine: 2.00
};
const price = prices[fruit] ?? "unknown";
console.log(`${fruit}: ${price}/lb`);
switch uses strict equality (===) and requires break to prevent fall-through. The 'orange' and 'tangerine' cases intentionally share code by omitting break between them. The object lookup pattern at the bottom is a popular modern alternative that avoids the verbosity and break pitfalls of switch.
// Basic ternary
const age = 20;
const status = age >= 18 ? "adult" : "minor";
console.log(status);
// Ternary in template literals
const count = 5;
console.log(`You have ${count} item${count === 1 ? "" : "s"}`);
// Ternary in function calls
function getGreeting(hour) {
return hour < 12 ? "Good morning" :
hour < 18 ? "Good afternoon" :
"Good evening";
}
console.log(getGreeting(9));
console.log(getGreeting(14));
console.log(getGreeting(20));
// Ternary for conditional assignment
const user = { name: "Alice", isAdmin: true };
const badge = user.isAdmin ? "Admin" : "User";
console.log(`${user.name} (${badge})`);
The ternary operator produces a value, making it ideal for assignments and interpolation. The getGreeting example shows chained ternaries - acceptable for simple linear conditions but avoid deeper nesting. When logic gets complex, switch to if/else.
// Short-circuit with &&
const isLoggedIn = true;
isLoggedIn && console.log("Welcome back!");
// Short-circuit with || for defaults
const userInput = "";
const displayName = userInput || "Anonymous";
console.log(displayName);
// Optional chaining (?.)
const user = {
name: "Alice",
address: {
street: "123 Main St"
}
};
console.log(user.address?.street); // "123 Main St"
console.log(user.address?.zipCode); // undefined (no error)
console.log(user.phone?.number); // undefined (no error)
// Combined optional chaining + nullish coalescing
const config = {};
const theme = config.ui?.theme ?? "light";
console.log(`Theme: ${theme}`);
// Optional chaining with methods
const arr = [1, 2, 3];
console.log(arr.find?.(x => x > 2)); // 3
const notArray = null;
console.log(notArray?.find?.(x => x > 2)); // undefined
Short-circuit patterns provide concise conditional logic. Optional chaining (?.) safely accesses nested properties without throwing errors when intermediate values are null or undefined. Combined with nullish coalescing (??), it creates robust patterns for handling missing data.
if/else, switch/case, Ternary Operator, Short-Circuit Evaluation, Conditional Patterns