Conditions and decisions
Control flow and functions14 min readLesson 1 of 4
1.1 Making a decision
A program becomes useful when it can choose. The if statement evaluates a condition, runs the block when the condition is true, and skips it otherwise. An optional else block runs when the condition is false.
Conditions are values. JavaScript converts the condition to a boolean with the truthiness rules: false, 0, -0, 0n, the empty string, null, undefined and NaN are falsy, and everything else is truthy. That conversion happens before the branch is chosen, so a condition that returns a non-boolean still works.
const score = 72;
if (score >= 60) {
console.log("Pass");
} else {
console.log("Try again");
}Output
Pass
Keep conditions boolean-shaped. if (score >= 60) reads as a question with a yes or no answer. if (score) reads as a question about truthiness, which is a different question. Both are legal, and the second is shorter only because it hides what is being tested.
The block form is optional for one statement, but braces make later edits safe. Adding a second statement to a braceless if silently moves it outside the condition, a bug that has cost many hours. Write braces even for one line.
1.2 else if chains
An else if chain tests conditions in order and runs the first branch that matches. The remaining branches are skipped, even if they would also match. Order therefore matters: put the most specific condition first and the catch-all else last.
The example below turns a numeric score into a letter grade. A score of 95 matches the first condition and never reaches the later tests. A score below 60 falls through to the final else.
function grade(score) {
if (score >= 90) return "A";
if (score >= 80) return "B";
if (score >= 70) return "C";
if (score >= 60) return "D";
return "F";
}
console.log(grade(95));
console.log(grade(72));
console.log(grade(41));Output
A C F
The function above uses early returns instead of an else if chain. Each return ends the function, so the later conditions only run when the earlier ones fail. Early returns flatten nested blocks and keep the happy path at the top, a style worth copying in longer functions.
A guard clause is the smallest early return: a check for invalid input that leaves the function before the real work starts. if (score < 0) return "invalid"; removes an edge case from everything that follows.
1.3 Nested conditions
Nested if statements read as a decision tree, and two levels are usually the limit before the code needs a new function or an early return. Combine conditions with && when both must hold, and with || when either can. Parentheses keep mixed logic readable.
For example, a checkout branch might require a logged-in user with a non-empty cart. Writing canCheckout(user, cart) as a function with a clear name hides the details and makes the branch easy to test. The condition deserves a name when it needs a comment.
function canCheckout(user, cart) {
return Boolean(user) && cart.length > 0;
}
const user = { name: "Aarav" };
const cart = ["pen", "notebook"];
if (canCheckout(user, cart)) {
console.log("Ready to pay");
} else {
console.log("Cart or login missing");
}Output
Ready to pay
1.4 switch
A switch compares one value against several cases with strict equality and runs the matching case. Without a break or return, execution falls through into the next case, which is a bug unless the fallthrough is deliberate. Each case label is a value, not a condition, so use switch for a fixed set of options and if for comparisons.
A default case is a good habit. It documents what happens for an unexpected value and catches data that arrives in a shape the code did not expect. Modern JavaScript sometimes replaces switch with an object lookup, which is compact when every branch returns a value.
function describeDay(day) {
switch (day) {
case "sat":
case "sun":
return "Weekend";
case "mon":
return "Start of the week";
default:
return "Weekday";
}
}
console.log(describeDay("sun"));
console.log(describeDay("wed"));Output
Weekend Weekday
1.5 Short branches with the conditional operator
When both branches only produce a value, the conditional operator is shorter than an if statement: const status = score >= 60 ? "pass" : "fail";. It reads as a question with two answers and works well inside template literals or property values.
The operator stops being helpful when the branches become expressions with side effects or when a second condition is needed. Nested conditional operators are hard to read, and an if statement with named branches is easier to debug. Use the ternary for a single either-or value, and keep the condition simple enough to read without brackets.
A common pattern assigns a default from a condition: const size = count > 10 ? "large" : "small";. The value has a name, the condition is visible, and the reader does not need to trace two assignment statements.
1.6 Comparing numbers and strings
Comparison operators work on numbers, strings and values that convert to them. Two strings compare by code unit order, so "10" < "9" is true because the character 1 comes before 9. Convert numeric strings with Number before comparing them, and compare dates as timestamps rather than as formatted text.
Boundary values deserve explicit tests. Off-by-one errors cluster around >= versus >, and around the length of a collection. When a condition checks a range, write both ends and test the exact boundary value in the console. The examples in this lesson use inclusive limits on purpose, because grades and ages usually include their boundary.
1.7 Validation with guard clauses
Form and API data arrives as strings and may be missing. Validate at the edge of the program: convert, trim and check before the value enters the rest of the code. A guard clause that returns early keeps the main path clean.
For example, a function that accepts a score can return a message for a non-number before applying grade thresholds. The check is cheap, and the rest of the function can assume a valid number. Guard clauses also make the failure path visible to a reader who only skims the top of the function.
When several checks are needed, a small validator function such as isValidScore(value) gives the condition a name. The branch then reads if (!isValidScore(score)) return "invalid";, and the validation rules live in one testable place.
1.8 Reading a condition aloud
A condition that cannot be read as a sentence usually hides a bug. if (user && user.active && !user.banned) reads as "the user exists, is active and is not banned". The same logic split across nested blocks takes longer to verify.
Name intermediate results when a condition grows. const canPost = user?.active && !user.banned; turns the test into a value with a name, and the if statement becomes if (canPost). This is the same technique as the checkout example, applied to any condition that needs a second reader to understand it.
1.9 Common mistakes
- Using an assignment inside a condition, so the branch tests the assigned value.
- Writing
if (value = 3)instead ofif (value === 3). - Forgetting break in a switch and letting cases fall through.
- Putting a broad condition first in an else if chain, so later branches never run.
- Testing a string input as a boolean and treating
"false"as false. - Nesting if blocks five levels deep instead of returning early.
Exam
A branch question usually gives a value and asks which line prints. Evaluate the conditions from top to bottom, stop at the first true one, and remember that the empty string and the string "0" are on opposite sides of the truthiness line.
- Write an if/else that prints different messages for a pass and a fail.
- Convert one if/else chain into early returns and compare readability.
- Rewrite a switch statement as an object lookup and test both versions.
- List the falsy values without checking, then verify.
