JavaScript — Control Flow
Control flow refers to the order in which individual statements, instructions, or function calls are executed or evaluated. JavaScript provides several constructs to control the flow of your program: conditional statements (if/else, switch, ternary), loops, and branching mechanisms.
Understanding control flow is fundamental to programming. Every non-trivial program needs to make decisions based on conditions, execute code paths conditionally, and handle multiple cases efficiently. This page covers all of JavaScript's control flow mechanisms in depth.
We will explore the decision-making constructs starting with the foundational if/else, moving through switch for multi-branch scenarios, the ternary operator for concise expressions, and the critical concept of truthy/falsy values that underpins all conditional logic in JavaScript.
| 1 | // Control flow determines which code paths execute |
| 2 | const temperature = 28; |
| 3 | let weather; |
| 4 | |
| 5 | if (temperature > 30) { |
| 6 | weather = "hot"; |
| 7 | } else if (temperature > 20) { |
| 8 | weather = "warm"; |
| 9 | } else if (temperature > 10) { |
| 10 | weather = "cool"; |
| 11 | } else { |
| 12 | weather = "cold"; |
| 13 | } |
| 14 | |
| 15 | console.log(weather); // "warm" |
| 16 | |
| 17 | // Every expression in JavaScript is truthy or falsy |
| 18 | // This determines whether conditions pass or fail |
| 19 | const name = "Alice"; |
| 20 | if (name) { |
| 21 | console.log("Name exists"); // runs because "Alice" is truthy |
| 22 | } |
| 23 | |
| 24 | const count = 0; |
| 25 | if (count) { |
| 26 | console.log("Count is truthy"); // never runs — 0 is falsy |
| 27 | } else { |
| 28 | console.log("Count is falsy"); // runs |
| 29 | } |
The if statement is the most basic and widely used control flow construct. It executes a block of code if a specified condition is truthy. The optional else block executes when the condition is falsy. Multiple conditions can be chained with else if.
| 1 | // Basic if/else |
| 2 | const age = 18; |
| 3 | |
| 4 | if (age >= 21) { |
| 5 | console.log("Can purchase alcohol"); |
| 6 | } else if (age >= 18) { |
| 7 | console.log("Can vote, but cannot purchase alcohol"); |
| 8 | } else if (age >= 16) { |
| 9 | console.log("Can drive"); |
| 10 | } else { |
| 11 | console.log("Minor"); |
| 12 | } |
| 13 | |
| 14 | // Nested conditions |
| 15 | const isAuthenticated = true; |
| 16 | const hasPermission = false; |
| 17 | const isOwner = true; |
| 18 | |
| 19 | if (isAuthenticated) { |
| 20 | if (hasPermission || isOwner) { |
| 21 | console.log("Access granted"); |
| 22 | } else { |
| 23 | console.log("Access denied — insufficient permissions"); |
| 24 | } |
| 25 | } else { |
| 26 | console.log("Please log in"); |
| 27 | } |
| 28 | |
| 29 | // Guard clause pattern — early return for edge cases |
| 30 | function processOrder(order) { |
| 31 | if (!order) { |
| 32 | return { error: "Order is required" }; |
| 33 | } |
| 34 | if (!order.items || order.items.length === 0) { |
| 35 | return { error: "Order must have items" }; |
| 36 | } |
| 37 | if (!order.customer) { |
| 38 | return { error: "Order must have a customer" }; |
| 39 | } |
| 40 | // Main logic — all guards passed |
| 41 | return { success: true, total: calculateTotal(order) }; |
| 42 | } |
| 43 | |
| 44 | // Chained conditions with logical operators |
| 45 | const score = 85; |
| 46 | if (score >= 90 && score <= 100) { |
| 47 | console.log("Grade: A"); |
| 48 | } else if (score >= 80 && score < 90) { |
| 49 | console.log("Grade: B"); |
| 50 | } else if (score >= 70 && score < 80) { |
| 51 | console.log("Grade: C"); |
| 52 | } else if (score >= 60 && score < 70) { |
| 53 | console.log("Grade: D"); |
| 54 | } else { |
| 55 | console.log("Grade: F"); |
| 56 | } |
| 57 | |
| 58 | // Conditionally executing with side effects |
| 59 | let isActive = true; |
| 60 | if (isActive) console.log("Active user"); // concise style (one-liner) |
| 61 | |
| 62 | // Multi-condition checks |
| 63 | const user = { role: "admin", isVerified: true }; |
| 64 | if (user.role === "admin" && user.isVerified) { |
| 65 | console.log("Admin access granted"); |
| 66 | } |
| 67 | |
| 68 | // Checking for null/undefined safely |
| 69 | const config = { timeout: 3000 }; |
| 70 | const timeout = config.timeout !== null && config.timeout !== undefined |
| 71 | ? config.timeout |
| 72 | : 5000; |
best practice
Conditional Execution Patterns
| 1 | // Pattern 1: Short-circuit with && |
| 2 | isLoggedIn && renderDashboard(); |
| 3 | |
| 4 | // Pattern 2: Short-circuit with || |
| 5 | const name = inputName || "Guest"; |
| 6 | |
| 7 | // Pattern 3: Nullish coalescing |
| 8 | const timeout = config.timeout ?? 5000; |
| 9 | |
| 10 | // Pattern 4: Ternary for assignment |
| 11 | const greeting = isMorning ? "Good morning" : "Hello"; |
| 12 | |
| 13 | // Pattern 5: Multi-ternary (use sparingly) |
| 14 | const status = score >= 90 ? "excellent" |
| 15 | : score >= 70 ? "good" |
| 16 | : score >= 50 ? "fair" |
| 17 | : "poor"; |
| 18 | |
| 19 | // Pattern 6: Object lookup instead of if/else chain |
| 20 | const rolePermissions = { |
| 21 | admin: ["read", "write", "delete", "manage"], |
| 22 | editor: ["read", "write"], |
| 23 | viewer: ["read"], |
| 24 | }; |
| 25 | const permissions = rolePermissions[user.role] || []; |
| 26 | |
| 27 | // Pattern 7: Using arrays for condition matching |
| 28 | const validRoles = ["admin", "editor", "viewer", "moderator"]; |
| 29 | if (validRoles.includes(user.role)) { |
| 30 | console.log("Valid role"); |
| 31 | } |
| 32 | |
| 33 | // Pattern 8: Dynamic function dispatch |
| 34 | const handlers = { |
| 35 | create: handleCreate, |
| 36 | update: handleUpdate, |
| 37 | delete: handleDelete, |
| 38 | }; |
| 39 | const handler = handlers[action] || handleUnknown; |
| 40 | handler(data); |
The switch statement evaluates an expression and executes the matching case clause. It uses strict comparison (===) to match values. The break statement exits the switch block; without it, execution "falls through" to the next case.
| 1 | // Basic switch statement |
| 2 | function getDayName(dayIndex) { |
| 3 | let dayName; |
| 4 | switch (dayIndex) { |
| 5 | case 0: |
| 6 | dayName = "Sunday"; |
| 7 | break; |
| 8 | case 1: |
| 9 | dayName = "Monday"; |
| 10 | break; |
| 11 | case 2: |
| 12 | dayName = "Tuesday"; |
| 13 | break; |
| 14 | case 3: |
| 15 | dayName = "Wednesday"; |
| 16 | break; |
| 17 | case 4: |
| 18 | dayName = "Thursday"; |
| 19 | break; |
| 20 | case 5: |
| 21 | dayName = "Friday"; |
| 22 | break; |
| 23 | case 6: |
| 24 | dayName = "Saturday"; |
| 25 | break; |
| 26 | default: |
| 27 | dayName = "Invalid day"; |
| 28 | } |
| 29 | return dayName; |
| 30 | } |
| 31 | |
| 32 | console.log(getDayName(3)); // "Wednesday" |
| 33 | |
| 34 | // Switch with fall-through (intentional) |
| 35 | function getDayType(dayIndex) { |
| 36 | switch (dayIndex) { |
| 37 | case 0: |
| 38 | case 6: |
| 39 | return "Weekend"; // fall-through for Sunday and Saturday |
| 40 | case 1: |
| 41 | case 2: |
| 42 | case 3: |
| 43 | case 4: |
| 44 | case 5: |
| 45 | return "Weekday"; |
| 46 | default: |
| 47 | return "Invalid"; |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | // Switch with shared logic for multiple cases |
| 52 | function handleHttpStatus(code) { |
| 53 | let message; |
| 54 | switch (true) { |
| 55 | case code >= 200 && code < 300: |
| 56 | message = "Success"; |
| 57 | break; |
| 58 | case code >= 300 && code < 400: |
| 59 | message = "Redirection"; |
| 60 | break; |
| 61 | case code >= 400 && code < 500: |
| 62 | message = "Client Error"; |
| 63 | break; |
| 64 | case code >= 500 && code < 600: |
| 65 | message = "Server Error"; |
| 66 | break; |
| 67 | default: |
| 68 | message = "Unknown Status"; |
| 69 | } |
| 70 | return message; |
| 71 | } |
| 72 | |
| 73 | // Switch with strict type checking |
| 74 | function parseCommand(input) { |
| 75 | switch (input) { |
| 76 | case "quit": |
| 77 | case "exit": |
| 78 | case "q": |
| 79 | process.exit(0); |
| 80 | break; |
| 81 | case "help": |
| 82 | case "h": |
| 83 | showHelp(); |
| 84 | break; |
| 85 | case "version": |
| 86 | case "v": |
| 87 | console.log("1.0.0"); |
| 88 | break; |
| 89 | default: |
| 90 | console.log(`Unknown command: ${input}`); |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | // Common mistake: forgetting break causes fall-through |
| 95 | function dangerousSwitch(val) { |
| 96 | let result = ""; |
| 97 | switch (val) { |
| 98 | case 1: |
| 99 | result += "A"; // no break! |
| 100 | case 2: |
| 101 | result += "B"; // no break! |
| 102 | case 3: |
| 103 | result += "C"; |
| 104 | break; |
| 105 | default: |
| 106 | result += "D"; |
| 107 | } |
| 108 | return result; |
| 109 | } |
| 110 | console.log(dangerousSwitch(1)); // "ABC" — unintended fall-through |
| 111 | console.log(dangerousSwitch(2)); // "BC" |
| 112 | console.log(dangerousSwitch(3)); // "C" |
warning
switch vs Object Lookup
| 1 | // Object lookup often replaces switch more cleanly |
| 2 | // Switch version |
| 3 | function getColorHex(type) { |
| 4 | switch (type) { |
| 5 | case "primary": return "#00FF41"; |
| 6 | case "secondary": return "#569CD6"; |
| 7 | case "danger": return "#EF4444"; |
| 8 | case "warning": return "#FFB000"; |
| 9 | default: return "#808080"; |
| 10 | } |
| 11 | } |
| 12 | |
| 13 | // Object lookup version |
| 14 | const colorMap = { |
| 15 | primary: "#00FF41", |
| 16 | secondary: "#569CD6", |
| 17 | danger: "#EF4444", |
| 18 | warning: "#FFB000", |
| 19 | }; |
| 20 | function getColorHex(type) { |
| 21 | return colorMap[type] || "#808080"; |
| 22 | } |
| 23 | |
| 24 | // Function lookup version (for logic, not just values) |
| 25 | const actionHandlers = { |
| 26 | increment: (state) => ({ count: state.count + 1 }), |
| 27 | decrement: (state) => ({ count: state.count - 1 }), |
| 28 | reset: () => ({ count: 0 }), |
| 29 | }; |
| 30 | function reducer(state, action) { |
| 31 | const handler = actionHandlers[action.type]; |
| 32 | return handler ? handler(state) : state; |
| 33 | } |
| 34 | |
| 35 | // When to use switch: |
| 36 | // 1. You need fall-through behavior |
| 37 | // 2. You have complex per-case logic |
| 38 | // 3. You need range checking (switch(true)) |
| 39 | // 4. Readability is improved by the explicit branching |
| 40 | |
| 41 | // When to use object lookup: |
| 42 | // 1. Simple value mapping |
| 43 | // 2. Dynamic dispatch based on runtime values |
| 44 | // 3. You want the map to be extensible at runtime |
| 45 | // 4. You need to share the map across functions |
The ternary operator (condition ? exprIfTrue : exprIfFalse) is the only JavaScript operator that takes three operands. It provides a concise way to write conditional expressions inline. Unlike if/else which is a statement, the ternary is an expression that produces a value.
| 1 | // Basic ternary — inline conditional expression |
| 2 | const age = 20; |
| 3 | const status = age >= 18 ? "Adult" : "Minor"; |
| 4 | console.log(status); // "Adult" |
| 5 | |
| 6 | // Ternary vs if/else — both same logic, different style |
| 7 | let message1; |
| 8 | if (isLoggedIn) { |
| 9 | message1 = "Welcome back!"; |
| 10 | } else { |
| 11 | message1 = "Please log in"; |
| 12 | } |
| 13 | |
| 14 | // Much cleaner as ternary |
| 15 | const message2 = isLoggedIn ? "Welcome back!" : "Please log in"; |
| 16 | |
| 17 | // Ternary for conditional rendering (React/JSX) |
| 18 | // {isLoading ? <Spinner /> : <Content />} |
| 19 | |
| 20 | // Nesting ternaries — be careful with readability |
| 21 | const grade = score >= 90 |
| 22 | ? "A" |
| 23 | : score >= 80 |
| 24 | ? "B" |
| 25 | : score >= 70 |
| 26 | ? "C" |
| 27 | : score >= 60 |
| 28 | ? "D" |
| 29 | : "F"; |
| 30 | |
| 31 | // Same thing with if/else |
| 32 | let grade2; |
| 33 | if (score >= 90) grade2 = "A"; |
| 34 | else if (score >= 80) grade2 = "B"; |
| 35 | else if (score >= 70) grade2 = "C"; |
| 36 | else if (score >= 60) grade2 = "D"; |
| 37 | else grade2 = "F"; |
| 38 | |
| 39 | // Ternary with function calls |
| 40 | const result = validate(input) |
| 41 | ? processValid(input) |
| 42 | : handleInvalid(input); |
| 43 | |
| 44 | // Conditional property access |
| 45 | const name = user ? user.name : "Anonymous"; |
| 46 | |
| 47 | // With nullish coalescing (cleaner for null/undefined) |
| 48 | const name2 = user?.name ?? "Anonymous"; |
| 49 | |
| 50 | // Ternary for class names or styles |
| 51 | const buttonClass = isActive |
| 52 | ? "bg-green-500 text-white" |
| 53 | : "bg-gray-200 text-gray-500"; |
| 54 | |
| 55 | // Avoid using ternary for side effects |
| 56 | // BAD — side effects in ternary |
| 57 | isError ? console.error(err) : console.log("OK"); |
| 58 | |
| 59 | // BETTER — use if/else for side effects |
| 60 | if (isError) { |
| 61 | console.error(err); |
| 62 | } else { |
| 63 | console.log("OK"); |
| 64 | } |
| 65 | |
| 66 | // Ternary with template literals |
| 67 | console.log(`Hello, ${user ? user.name : "Guest"}!`); |
| 68 | |
| 69 | // Returning from ternary |
| 70 | function getAccessLevel(user) { |
| 71 | return user.isAdmin |
| 72 | ? "admin" |
| 73 | : user.isModerator |
| 74 | ? "moderator" |
| 75 | : "user"; |
| 76 | } |
pro tip
Every value in JavaScript has an inherent boolean "truthiness" when evaluated in a boolean context (like an if condition). Values that coerce to true are truthy; those that coerce to false are falsy. Understanding this is essential for writing correct conditional logic.
| Value | Type | Truthy/Falsy | Notes |
|---|---|---|---|
| false | Boolean | Falsy | The literal false value |
| 0 | Number | Falsy | The number zero |
| -0 | Number | Falsy | Negative zero |
| 0n | BigInt | Falsy | BigInt zero |
| "" | String | Falsy | Empty string |
| null | Null | Falsy | Intentional absence |
| undefined | Undefined | Falsy | Uninitialized/missing |
| NaN | Number | Falsy | Not a Number |
| true | Boolean | Truthy | The literal true value |
| 42 | Number | Truthy | Any non-zero number |
| "hello" | String | Truthy | Any non-empty string |
| [] | Object | Truthy | Empty array (still an object) |
| Object | Truthy | Empty object (still an object) | |
| Infinity | Number | Truthy | Positive infinity |
| new Date() | Object | Truthy | All Date instances |
| 1 | // The complete list of falsy values in JavaScript |
| 2 | // Only 7 values — everything else is truthy |
| 3 | |
| 4 | const falsyValues = [ |
| 5 | false, // the boolean false |
| 6 | 0, // the number zero |
| 7 | -0, // negative zero |
| 8 | 0n, // BigInt zero |
| 9 | "", // empty string (also '' and ``) |
| 10 | null, // null |
| 11 | undefined, // undefined |
| 12 | NaN, // Not a Number |
| 13 | ]; |
| 14 | |
| 15 | falsyValues.forEach(val => { |
| 16 | console.log(Boolean(val)); // false for all |
| 17 | }); |
| 18 | |
| 19 | // Truthy examples — this might surprise you |
| 20 | console.log(Boolean([])); // true (empty array) |
| 21 | console.log(Boolean({})); // true (empty object) |
| 22 | console.log(Boolean("false")); // true (non-empty string!) |
| 23 | console.log(Boolean("0")); // true (non-empty string!) |
| 24 | console.log(Boolean(" ")); // true (whitespace is a string) |
| 25 | console.log(Boolean(Infinity)); // true |
| 26 | console.log(Boolean(-Infinity)); // true |
| 27 | |
| 28 | // How truthiness affects control flow |
| 29 | const input = ""; // empty string |
| 30 | |
| 31 | if (input) { |
| 32 | console.log("Input is truthy"); // never runs |
| 33 | } else { |
| 34 | console.log("Input is falsy"); // runs |
| 35 | } |
| 36 | |
| 37 | // Common gotcha: checking array length |
| 38 | const items = []; |
| 39 | if (items) { |
| 40 | console.log("Items is truthy"); // runs! (empty array is truthy) |
| 41 | } |
| 42 | if (items.length) { |
| 43 | console.log("Has items"); // never runs (length is 0) |
| 44 | } |
| 45 | |
| 46 | // Use Boolean() or !! for explicit coercion |
| 47 | console.log(Boolean(0)); // false |
| 48 | console.log(!!0); // false |
| 49 | console.log(Boolean("0")); // true |
| 50 | console.log(!!"0"); // true |
| 51 | |
| 52 | // Truthiness in logical operators |
| 53 | const name = userInput || "default"; // catches all falsy |
| 54 | const name2 = userInput ?? "default"; // only null/undefined |
| 55 | |
| 56 | // Checking for existence — beware of 0 and "" |
| 57 | function getItems(count) { |
| 58 | // BUG: if count is 0, this returns "default" |
| 59 | return count || "default"; |
| 60 | } |
| 61 | console.log(getItems(0)); // "default" — probably wrong! |
| 62 | console.log(getItems(5)); // 5 |
| 63 | |
| 64 | // FIX: explicit null/undefined check |
| 65 | function getItems(count) { |
| 66 | return count ?? "default"; // count ?? "default" |
| 67 | } |
| 68 | // Or check for undefined explicitly |
| 69 | function getItems(count) { |
| 70 | return count !== undefined ? count : "default"; |
| 71 | } |
danger
Writing clean control flow is a hallmark of experienced developers. These practices will help you avoid common bugs and produce more readable code.
best practice
Refactoring Complex Control Flow
| 1 | // BEFORE — deeply nested, hard to follow |
| 2 | function processOrder(order) { |
| 3 | if (order) { |
| 4 | if (order.items && order.items.length > 0) { |
| 5 | if (order.customer) { |
| 6 | if (order.customer.address) { |
| 7 | if (order.payment) { |
| 8 | if (order.payment.status === "confirmed") { |
| 9 | return shipOrder(order); |
| 10 | } else { |
| 11 | return { error: "Payment not confirmed" }; |
| 12 | } |
| 13 | } else { |
| 14 | return { error: "Payment required" }; |
| 15 | } |
| 16 | } else { |
| 17 | return { error: "Address required" }; |
| 18 | } |
| 19 | } else { |
| 20 | return { error: "Customer required" }; |
| 21 | } |
| 22 | } else { |
| 23 | return { error: "Cart is empty" }; |
| 24 | } |
| 25 | } else { |
| 26 | return { error: "Order is null" }; |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | // AFTER — guard clauses, flat structure, clear |
| 31 | function processOrder(order) { |
| 32 | if (!order) return { error: "Order is null" }; |
| 33 | if (!order.items?.length) return { error: "Cart is empty" }; |
| 34 | if (!order.customer) return { error: "Customer required" }; |
| 35 | if (!order.customer.address) return { error: "Address required" }; |
| 36 | if (!order.payment) return { error: "Payment required" }; |
| 37 | if (order.payment.status !== "confirmed") { |
| 38 | return { error: "Payment not confirmed" }; |
| 39 | } |
| 40 | return shipOrder(order); |
| 41 | } |
| 42 | |
| 43 | // Rule of thumb: extract complex conditions |
| 44 | function isEligibleForDiscount(user, order, campaign) { |
| 45 | // Complex logic extracted into a well-named function |
| 46 | if (!user.isVerified) return false; |
| 47 | if (order.total < 50) return false; |
| 48 | if (campaign.expired) return false; |
| 49 | if (user.tier === "vip") return true; |
| 50 | if (campaign.code && !user.usedCampaigns?.includes(campaign.code)) return true; |
| 51 | return false; |
| 52 | } |
| 53 | |
| 54 | // Compose conditions from smaller functions |
| 55 | const canCheckout = ( |
| 56 | hasItems(order) && |
| 57 | isValidAddress(order.shipping) && |
| 58 | isPaymentMethodAccepted(order.payment) |
| 59 | ); |