|$ curl https://forge-ai.dev/api/markdown?path=docs/js/control-flow
$cat docs/javascript-—-control-flow.md
updated This week·20 min read·published

JavaScript — Control Flow

JavaScriptControl FlowBeginnerBeginner to Advanced
Introduction

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.

control-flow-intro.js
JavaScript
1// Control flow determines which code paths execute
2const temperature = 28;
3let weather;
4
5if (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
15console.log(weather); // "warm"
16
17// Every expression in JavaScript is truthy or falsy
18// This determines whether conditions pass or fail
19const name = "Alice";
20if (name) {
21 console.log("Name exists"); // runs because "Alice" is truthy
22}
23
24const count = 0;
25if (count) {
26 console.log("Count is truthy"); // never runs — 0 is falsy
27} else {
28 console.log("Count is falsy"); // runs
29}
The if/else Statement

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.

if-else.js
JavaScript
1// Basic if/else
2const age = 18;
3
4if (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
15const isAuthenticated = true;
16const hasPermission = false;
17const isOwner = true;
18
19if (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
30function 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
45const score = 85;
46if (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
59let isActive = true;
60if (isActive) console.log("Active user"); // concise style (one-liner)
61
62// Multi-condition checks
63const user = { role: "admin", isVerified: true };
64if (user.role === "admin" && user.isVerified) {
65 console.log("Admin access granted");
66}
67
68// Checking for null/undefined safely
69const config = { timeout: 3000 };
70const timeout = config.timeout !== null && config.timeout !== undefined
71 ? config.timeout
72 : 5000;

best practice

Use the guard clause pattern (early returns) to flatten deeply nested if/else blocks. Instead of wrapping main logic in an if statement, check error/edge conditions first and return early. This reduces nesting, improves readability, and makes the main code path obvious. Aim for no more than two levels of nesting.

Conditional Execution Patterns

conditional-patterns.js
JavaScript
1// Pattern 1: Short-circuit with &&
2isLoggedIn && renderDashboard();
3
4// Pattern 2: Short-circuit with ||
5const name = inputName || "Guest";
6
7// Pattern 3: Nullish coalescing
8const timeout = config.timeout ?? 5000;
9
10// Pattern 4: Ternary for assignment
11const greeting = isMorning ? "Good morning" : "Hello";
12
13// Pattern 5: Multi-ternary (use sparingly)
14const 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
20const rolePermissions = {
21 admin: ["read", "write", "delete", "manage"],
22 editor: ["read", "write"],
23 viewer: ["read"],
24};
25const permissions = rolePermissions[user.role] || [];
26
27// Pattern 7: Using arrays for condition matching
28const validRoles = ["admin", "editor", "viewer", "moderator"];
29if (validRoles.includes(user.role)) {
30 console.log("Valid role");
31}
32
33// Pattern 8: Dynamic function dispatch
34const handlers = {
35 create: handleCreate,
36 update: handleUpdate,
37 delete: handleDelete,
38};
39const handler = handlers[action] || handleUnknown;
40handler(data);
The switch Statement

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.

switch.js
JavaScript
1// Basic switch statement
2function 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
32console.log(getDayName(3)); // "Wednesday"
33
34// Switch with fall-through (intentional)
35function 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
52function 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
74function 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
95function 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}
110console.log(dangerousSwitch(1)); // "ABC" — unintended fall-through
111console.log(dangerousSwitch(2)); // "BC"
112console.log(dangerousSwitch(3)); // "C"

warning

The fall-through behavior (executing the next case without break) is frequently a source of bugs. If you intentionally use fall-through, add a comment explaining why. Some linters can enforce that every non-empty case has a break. The default clause should usually be the last case and does not require a break.

switch vs Object Lookup

switch-vs-object.js
JavaScript
1// Object lookup often replaces switch more cleanly
2// Switch version
3function 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
14const colorMap = {
15 primary: "#00FF41",
16 secondary: "#569CD6",
17 danger: "#EF4444",
18 warning: "#FFB000",
19};
20function getColorHex(type) {
21 return colorMap[type] || "#808080";
22}
23
24// Function lookup version (for logic, not just values)
25const actionHandlers = {
26 increment: (state) => ({ count: state.count + 1 }),
27 decrement: (state) => ({ count: state.count - 1 }),
28 reset: () => ({ count: 0 }),
29};
30function 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
Ternary Operator

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.

ternary.js
JavaScript
1// Basic ternary — inline conditional expression
2const age = 20;
3const status = age >= 18 ? "Adult" : "Minor";
4console.log(status); // "Adult"
5
6// Ternary vs if/else — both same logic, different style
7let message1;
8if (isLoggedIn) {
9 message1 = "Welcome back!";
10} else {
11 message1 = "Please log in";
12}
13
14// Much cleaner as ternary
15const 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
21const 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
32let grade2;
33if (score >= 90) grade2 = "A";
34else if (score >= 80) grade2 = "B";
35else if (score >= 70) grade2 = "C";
36else if (score >= 60) grade2 = "D";
37else grade2 = "F";
38
39// Ternary with function calls
40const result = validate(input)
41 ? processValid(input)
42 : handleInvalid(input);
43
44// Conditional property access
45const name = user ? user.name : "Anonymous";
46
47// With nullish coalescing (cleaner for null/undefined)
48const name2 = user?.name ?? "Anonymous";
49
50// Ternary for class names or styles
51const 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
57isError ? console.error(err) : console.log("OK");
58
59// BETTER — use if/else for side effects
60if (isError) {
61 console.error(err);
62} else {
63 console.log("OK");
64}
65
66// Ternary with template literals
67console.log(`Hello, ${user ? user.name : "Guest"}!`);
68
69// Returning from ternary
70function getAccessLevel(user) {
71 return user.isAdmin
72 ? "admin"
73 : user.isModerator
74 ? "moderator"
75 : "user";
76}
🔥

pro tip

A good rule of thumb: use the ternary operator for simple binary assignments where the condition and both branches are short (one line each). For anything more complex — nested ternaries, side effects, or multi-line branches — use if/else. Readability should always trump brevity.
Truthy & Falsy Values

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.

ValueTypeTruthy/FalsyNotes
falseBooleanFalsyThe literal false value
0NumberFalsyThe number zero
-0NumberFalsyNegative zero
0nBigIntFalsyBigInt zero
""StringFalsyEmpty string
nullNullFalsyIntentional absence
undefinedUndefinedFalsyUninitialized/missing
NaNNumberFalsyNot a Number
trueBooleanTruthyThe literal true value
42NumberTruthyAny non-zero number
"hello"StringTruthyAny non-empty string
[]ObjectTruthyEmpty array (still an object)
ObjectTruthyEmpty object (still an object)
InfinityNumberTruthyPositive infinity
new Date()ObjectTruthyAll Date instances
truthy-falsy.js
JavaScript
1// The complete list of falsy values in JavaScript
2// Only 7 values — everything else is truthy
3
4const 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
15falsyValues.forEach(val => {
16 console.log(Boolean(val)); // false for all
17});
18
19// Truthy examples — this might surprise you
20console.log(Boolean([])); // true (empty array)
21console.log(Boolean({})); // true (empty object)
22console.log(Boolean("false")); // true (non-empty string!)
23console.log(Boolean("0")); // true (non-empty string!)
24console.log(Boolean(" ")); // true (whitespace is a string)
25console.log(Boolean(Infinity)); // true
26console.log(Boolean(-Infinity)); // true
27
28// How truthiness affects control flow
29const input = ""; // empty string
30
31if (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
38const items = [];
39if (items) {
40 console.log("Items is truthy"); // runs! (empty array is truthy)
41}
42if (items.length) {
43 console.log("Has items"); // never runs (length is 0)
44}
45
46// Use Boolean() or !! for explicit coercion
47console.log(Boolean(0)); // false
48console.log(!!0); // false
49console.log(Boolean("0")); // true
50console.log(!!"0"); // true
51
52// Truthiness in logical operators
53const name = userInput || "default"; // catches all falsy
54const name2 = userInput ?? "default"; // only null/undefined
55
56// Checking for existence — beware of 0 and ""
57function getItems(count) {
58 // BUG: if count is 0, this returns "default"
59 return count || "default";
60}
61console.log(getItems(0)); // "default" — probably wrong!
62console.log(getItems(5)); // 5
63
64// FIX: explicit null/undefined check
65function getItems(count) {
66 return count ?? "default"; // count ?? "default"
67}
68// Or check for undefined explicitly
69function getItems(count) {
70 return count !== undefined ? count : "default";
71}

danger

The most common truthiness bug: empty arrays and objects are truthy. if (array) is NOT the same as if (array.length > 0). Always check .length for arrays and use Object.keys(obj).length for objects. Also remember that the string false is truthy — only the boolean false is falsy.
preview
Control Flow Best Practices

Writing clean control flow is a hallmark of experienced developers. These practices will help you avoid common bugs and produce more readable code.

Prefer guard clauses (early return) to reduce nesting — keep the main path at the top level
Use switch for 3+ conditions on the same value; use if/else for range checks or diverse conditions
Never use the ternary operator for side effects — use if/else for statements, ternary for expressions
Always use === in conditions to avoid type coercion surprises; never use == in control flow
Check array.length, not the array itself — empty arrays are truthy
Use ?? instead of || when 0 or empty string are valid values that should not trigger defaults
Prefer object lookups over long if/else or switch chains for simple value mapping
Avoid deep nesting (3+ levels) — extract conditions into well-named helper functions
Add explicit comments for intentional fall-through in switch statements
Use logical operators (&&, ||) for simple conditional execution, but never at the expense of readability

best practice

The golden rule of control flow: your code should read like a story. The happy path should be the most visible — put it in the main branch, not buried inside nested conditions. Use descriptive variable names for conditions (isAuthenticated over user !== null) so the logic is self-documenting.

Refactoring Complex Control Flow

refactoring.js
JavaScript
1// BEFORE — deeply nested, hard to follow
2function 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
31function 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
44function 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
55const canCheckout = (
56 hasItems(order) &&
57 isValidAddress(order.shipping) &&
58 isPaymentMethodAccepted(order.payment)
59);
$Blueprint — Engineering Documentation·Section ID: JS-CONTROLFLOW·Revision: 1.0