047: Control Flow: Conditions and Branching
Outcomes
By the end of this lesson, you can:
- use
if,else if, andelseto select behavior; - write readable business rules with strict comparisons and logical operators;
- identify common truthy and falsy values;
- use a ternary expression for a simple two-value choice; and
- apply guard conditions to reject invalid states early.
Prerequisites and Retrieval
Retrieve variables, types, coercion/equality, and operators from 043–046.
- What is the difference between
=and===? - What does
age >= 18 && hasIdmean? - Why is
"18" === 18false?
Run examples in a browser console or module script. Change one input at a time and predict which branch will execute.
Terms
- condition: An expression evaluated as true/false to choose a branch. — Source: MDN: if...else
- branch: One alternative path selected among conditionals. — Source: MDN: if...else
- control flow: The order statements execute, shaped by conditions and loops. — Source: MDN: Control flow and error handling
ifstatement: Runs its block only when the parenthesized condition is truthy. — Source: MDN: if...elseelse if: Chains an additional condition tested only when earlier ones fail. — Source: MDN: if...elseelse: Fallback branch executed when no preceding condition matched. — Source: MDN: if...else- truthy/falsy: Values coerced to true/false in Boolean context; falsy list includes 0, "", null, undefined, NaN. — Source: MDN: Truthy / Falsy
- ternary operator: cond ? a : b — inline conditional expression choosing between two results. — Source: MDN: Conditional operator
- guard condition: Early check returning/exiting before main logic handles invalid input. — Source: MDN: Control flow guide
- boundary: Edge input values where behavior may change (0, empty string, limits). — Source: MDN: Control flow guide
- Boolean logic (official): "Boolean logic uses true/false with operators &&, ||, !." — Source: MDN: Logical operators
- Guard clause: "A guard clause is an early return that handles a special case before the main logic." — Source: MDN: Guard clause pattern — Control flow
Beginner Explanation and Mental Model
A condition is a fork in the program's road. JavaScript evaluates a question. If its result is truthy, it executes one block; otherwise it skips that block. Braces group the statements belonging to a branch:
if (temperature > 30) {
console.log("Hot day");
} else {
console.log("Not a hot day");
}
Always use braces, even for one statement. They make branch boundaries visible and prevent errors when another statement is added.
An else if chain tests from top to bottom and executes only the first matching branch:
if (score >= 90) {
console.log("A");
} else if (score >= 75) {
console.log("B");
} else {
console.log("C");
}
Order matters. The highest threshold comes first. If score >= 75 came first, a score of 95 would enter that branch and never reach the >= 90 test.
Before coding a multi-branch rule, make a decision table. List representative inputs, the expected branch, and the expected result. For the grading example, rows might include 95/A, 82/B, 63/C, and 40/F. Then add boundary rows such as 89/90 and 74/75. A table separates the business rule from JavaScript syntax and gives you a quick way to check, "Which row should this input match?" During a trace, evaluate one condition at a time and stop at the first truthy condition, exactly as the engine does.
Truthy and falsy
Conditions accept any value. These common application values are falsy: false, 0, -0, 0n, "", null, undefined, and NaN. Values such as "false", "0", empty arrays, and empty objects are truthy. A legacy browser-only value, document.all, is a historical exception to simple exhaustive claims about falsy values; application code should not use it. Prefer explicit comparisons when 0 or an empty string has a legitimate meaning:
if (itemCount === 0) {
console.log("Cart is empty");
}
This communicates more than if (!itemCount) and does not accidentally treat NaN as an empty cart.
Ternary choices
The conditional operator is an expression that produces one of two values:
const label = isAvailable ? "In stock" : "Sold out";
Use it for a short, simple choice. Use if/else for multiple statements or several branches. Avoid nested ternaries because they hide control flow.
Guards
A guard handles a bad or special case before normal logic. At top level it can be an outer condition; inside a function it often returns early. Until functions arrive, write:
if (
typeof score !== "number" ||
!Number.isFinite(score) ||
score < 0 ||
score > 100
) {
console.log("Invalid score");
} else {
// Normal grading logic
}
Worked Example: Grade Calculator
const score = 82;
let grade;
let message;
if (
typeof score !== "number" ||
!Number.isFinite(score) ||
score < 0 ||
score > 100
) {
grade = "Invalid";
message = "Score must be from 0 to 100.";
} else if (score >= 90) {
grade = "A";
message = "Excellent";
} else if (score >= 75) {
grade = "B";
message = "Good work";
} else if (score >= 60) {
grade = "C";
message = "Passed";
} else {
grade = "F";
message = "Needs improvement";
}
console.log("Score:", score);
console.log("Grade:", grade);
console.log("Message:", message);
Expected output:
Score: 82 Grade: B Message: Good work
grade and message use let because each is declared first and assigned in exactly one selected branch. The guard rejects non-numbers, NaN, positive or negative Infinity, and values outside 0-100 before any grading comparison. For a valid score, descending thresholds avoid upper-bound repetition: when execution reaches score >= 75, the earlier >= 90 test has already failed.
Test invalid types and numeric boundaries: "82", NaN, Infinity, -Infinity, -1, 0, 59, 60, 74, 75, 89, 90, 100, and 101. These checks catch coercion, non-finite-number, and > versus >= mistakes.
Intermediate Example: Event Eligibility
const age = 20;
const hasTicket = true;
const hasPhotoId = true;
const isBanned = false;
const meetsAgeRequirement = age >= 18;
const hasEntryDocuments = hasTicket && hasPhotoId;
const canEnter = meetsAgeRequirement && hasEntryDocuments && !isBanned;
const status = canEnter ? "Entry approved" : "Entry denied";
console.log(status);
if (!meetsAgeRequirement) {
console.log("Reason: minimum age is 18.");
} else if (!hasTicket) {
console.log("Reason: ticket required.");
} else if (!hasPhotoId) {
console.log("Reason: photo ID required.");
} else if (isBanned) {
console.log("Reason: account is banned.");
} else {
console.log("All entry rules passed.");
}
Expected output:
Entry approved All entry rules passed.
Named Boolean variables make the policy readable. The combined expression answers whether entry is allowed; the branch chain explains the first failing reason. This separation is easier to maintain than repeating one giant condition.
Optional Advanced Extension: Boolean Conversion
Boolean(value) exposes truthiness without a branch:
console.log(Boolean("")); // false
console.log(Boolean("false")); // true
console.log(Boolean(0)); // false
console.log(Boolean([])); // true
Use this to understand rules, not to replace clear domain checks. !!value produces the same Boolean conversion but is less beginner-friendly. Never use new Boolean(false): it creates an object, and objects are truthy.
Mistakes and Debugging
- Assignment in a condition:
if (role = "admin")changesrole. Userole === "admin". - Wrong branch order: place specific or higher thresholds before broad/lower ones.
- Missing boundary: decide deliberately whether the threshold itself belongs with
>or>=. - Comparing numeric input as text: validate and convert input before rules.
- Assuming
"false"is falsy: it is non-empty and therefore truthy. - Overly dense logic: name intermediate Boolean expressions.
- Nested ternaries: replace them with
if/else if/else. - Independent
ifstatements when only one result is allowed: use anelse ifchain; separateifstatements can all execute. - Missing braces: add them consistently.
Debug by logging each input and named rule. Determine which branch should be first, and test immediately below, at, and above every boundary.
When output is wrong but no error appears, distinguish a syntax problem from a logic problem. Syntax problems stop parsing and produce an error. Logic problems run successfully but select the wrong branch. For logic problems, temporarily log each condition, for example console.log(score >= 90, score >= 75);. Compare those Booleans with the decision table, fix the earliest disagreement, and then remove temporary diagnostics.
Best Practices
- Use strict equality and explicit comparisons.
- Name business rules such as
isEligibleandmeetsMinimum. - Put validation or exceptional cases before normal paths.
- Keep branch bodies short and avoid deep nesting.
- Order mutually exclusive thresholds carefully.
- Use a ternary only for one uncomplicated value choice.
- Do not depend on truthiness when valid values include
0or"". - Avoid repeating the same condition in multiple branches.
Tiered Exercises
Core
Given an age, print Child for under 13, Teen for 13-17, and Adult for 18 or older. Reject negative ages.
Practice
Build a discount calculator. Orders at least 2000 receive 15%, orders at least 1000 receive 10%, otherwise no discount. Reject negative totals and print original total, discount, and final total.
Professional Extension
An applicant is eligible when age is 18-60 inclusive, documents are verified, and status is not "blocked". Compute named rules, produce an eligibility label with a ternary, and use branches to explain the first failure.
Complete Solutions
const age = 16;
if (age < 0) {
console.log("Invalid age");
} else if (age < 13) {
console.log("Child");
} else if (age < 18) {
console.log("Teen");
} else {
console.log("Adult");
}
Expected output: Teen.
const orderTotal = 1600;
let discountRate;
if (orderTotal < 0) {
console.log("Invalid order total");
} else {
if (orderTotal >= 2000) {
discountRate = 0.15;
} else if (orderTotal >= 1000) {
discountRate = 0.10;
} else {
discountRate = 0;
}
const discount = orderTotal * discountRate;
const finalTotal = orderTotal - discount;
console.log("Original:", orderTotal);
console.log("Discount:", discount);
console.log("Final:", finalTotal);
}
Expected final total: 1440.
const age = 27;
const documentsVerified = true;
const status = "active";
const isAgeAllowed = age >= 18 && age <= 60;
const isNotBlocked = status !== "blocked";
const isEligible = isAgeAllowed && documentsVerified && isNotBlocked;
const label = isEligible ? "Eligible" : "Not eligible";
console.log(label);
if (!isAgeAllowed) {
console.log("Age must be from 18 to 60.");
} else if (!documentsVerified) {
console.log("Documents are not verified.");
} else if (!isNotBlocked) {
console.log("Status is blocked.");
} else {
console.log("All checks passed.");
}
Recap and Exit Questions
Conditions choose paths. if/else if/else supports mutually exclusive branches, truthiness converts values for a Boolean context, ternaries produce a simple two-way value, and guards keep invalid cases away from normal logic.
- Why does branch order matter in grading?
- Name five falsy values.
- When is a ternary clearer than
if/else? - Why should
0often be checked explicitly? - Which boundary values would you test for
age >= 18?
Official References
- MDN: Control flow and error handling
- MDN:
if...else - MDN: falsy
- MDN: conditional operator
- ECMA-262:
ifstatement
References checked 2026-08-24.
