FullStack Course LogoFullStack Course
Module: JavaScript
JavaScript·047·9 MIN READ

047: Control Flow: Conditions and Branching

TOPICS COVERED: Control Flow: Conditions and Branching

Outcomes

By the end of this lesson, you can:

  • use if, else if, and else to 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

Before starting, bring back the ideas from lessons 043–046: variables, types, coercion and equality, and operators. These are the pieces that conditions combine into a decision.

  1. What is the difference between = and ===?
  2. What does age >= 18 && hasId mean?
  3. Why is "18" === 18 false?

Run the examples in a browser console or a module script. Change one input at a time, predict the branch that should execute, and then compare your prediction with JavaScript's result.

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
  • if statement: Runs its block only when the parenthesized condition is truthy. — Source: MDN: if...else
  • else if: Chains an additional condition tested only when earlier ones fail. — Source: MDN: if...else
  • else: 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

Most programs do not follow one straight path. They inspect the current state, choose an alternative, and continue from there. A condition is that decision point: JavaScript evaluates an expression, and if the result is truthy, it runs the associated block. If the result is falsy, it skips that block. Braces make the statements belonging to each branch explicit:

js
if (temperature > 30) {
  console.log("Hot day");
} else {
  console.log("Not a hot day");
}

Use braces even when a branch currently contains one statement. The extra pair makes the boundary visible and prevents a subtle bug when someone adds another statement later and assumes it is also conditional.

An else if chain is tested from top to bottom. As soon as one condition is truthy, JavaScript executes that branch and skips the rest of the chain:

js
if (score >= 90) {
  console.log("A");
} else if (score >= 75) {
  console.log("B");
} else {
  console.log("C");
}

That order is part of the rule, not just formatting. The highest threshold belongs first. If score >= 75 came before score >= 90, a score of 95 would enter the >= 75 branch and never get as far as the A test.

For a multi-branch rule, write a decision table before writing the conditional when the cases are important or easy to confuse. List representative inputs, the branch each input should select, and the expected result. For the grading example, rows might include 95/A, 82/B, 63/C, and 40/F. Add boundary rows such as 89/90 and 74/75 as well. This separates the business rule from JavaScript syntax and gives you a concrete answer to, "Which row should this input match?" When tracing the code, evaluate one condition at a time and stop at the first truthy condition, just as the engine does.

Truthy and falsy

An if condition does not require an expression that literally returns true or false; JavaScript converts any value used in a Boolean context. The common falsy values are false, 0, -0, 0n, "", null, undefined, and NaN. Values that often surprise beginners, including "false", "0", empty arrays, and empty objects, are truthy because they are non-empty strings or objects. A legacy browser-only value, document.all, is a historical exception to simple exhaustive claims about falsy values; application code should not use it.

When 0 or an empty string is a legitimate value in your domain, prefer an explicit comparison:

js
if (itemCount === 0) {
  console.log("Cart is empty");
}

This says exactly what the rule means. if (!itemCount) is shorter, but it also treats NaN as falsy and can make a valid empty-string or zero case look like a missing value.

Ternary choices

The conditional, or ternary, operator is an expression. It evaluates a condition and produces one of two values:

js
const label = isAvailable ? "In stock" : "Sold out";

It works well when the choice is short and uncomplicated. Use if/else when a branch needs multiple statements or when there are several alternatives. Nested ternaries are usually a poor trade: they compress the text while making the control flow harder to read and debug.

Guards

A guard deals with a bad or special case before the normal logic starts. At the top level, it can be the outer condition of an if/else. Inside a function, the same idea commonly becomes an early return. Until functions are introduced, the structure looks like this:

js
if (
  typeof score !== "number" ||
  !Number.isFinite(score) ||
  score < 0 ||
  score > 100
) {
  console.log("Invalid score");
} else {
  // Normal grading logic
}

The normal path is easier to reason about because invalid input has already been separated from it. Notice that the guard checks both the type and the numeric range; a value can be a number and still be NaN, infinite, or outside the allowed interval.

Worked Example: Grade Calculator

Here is a complete grading rule. Validation comes first, followed by mutually exclusive score ranges:

js
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:

text
Score: 82
Grade: B
Message: Good work

grade and message use let because each variable is declared before the conditional and assigned in exactly one selected branch. The guard rejects non-numbers, NaN, positive and negative Infinity, and values outside 0–100 before any grading comparison runs. For a valid score, descending thresholds avoid repeating upper bounds: when execution reaches score >= 75, the earlier >= 90 test has already failed.

Test more than the happy path. Try the invalid type "82", NaN, Infinity, -Infinity, -1, and 101, along with the boundaries 0, 59, 60, 74, 75, 89, 90, and 100. These inputs expose coercion mistakes, failures to reject non-finite numbers, and errors involving > versus >=.

Intermediate Example: Event Eligibility

The next example has two related jobs: one expression calculates whether entry is allowed, and a branch chain explains the first rule that failed.

js
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:

text
Entry approved
All entry rules passed.

Named Boolean variables turn a dense policy into readable pieces. The combined expression answers the approval question, while the branch chain explains the first failing reason in a deliberate order. That separation is easier to maintain than repeating one giant condition in several places, and it gives you individual values to inspect while debugging.

Optional Advanced Extension: Boolean Conversion

Boolean(value) lets you inspect JavaScript's truthiness conversion without writing a branch:

js
console.log(Boolean(""));      // false
console.log(Boolean("false")); // true
console.log(Boolean(0));       // false
console.log(Boolean([]));      // true

Use this to understand a rule, not as a substitute for a clear domain check. !!value performs the same Boolean conversion but is less approachable when someone is still learning the language. Never use new Boolean(false) for this purpose: it creates an object, and objects are truthy.

Mistakes and Debugging

These failures show up often in conditional code:

  • Assignment in a condition: if (role = "admin") changes role. Use role === "admin".
  • Wrong branch order: place specific or higher thresholds before broad or lower ones.
  • Missing boundary: decide deliberately whether the threshold itself belongs with > or >=.
  • Comparing numeric input as text: validate and convert input before applying the rules.
  • Assuming "false" is falsy: it is a non-empty string and therefore truthy.
  • Overly dense logic: name intermediate Boolean expressions.
  • Nested ternaries: replace them with if/else if/else.
  • Independent if statements when only one result is allowed: use an else if chain; separate if statements can all execute.
  • Missing braces: add them consistently.

Start debugging by logging each input and each named rule. Work out which branch should be selected first, then test immediately below, exactly at, and immediately above every boundary.

When the output is wrong but no error appears, separate a syntax problem from a logic problem. A syntax problem prevents parsing and produces an error. A logic problem runs successfully but selects the wrong branch. For a logic problem, temporarily log each condition, for example console.log(score >= 90, score >= 75);. Compare those Boolean results with your decision table, fix the earliest disagreement, and remove the temporary diagnostics afterward.

Best Practices

  • Use strict equality and explicit comparisons.
  • Name business rules such as isEligible and meetsMinimum.
  • 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 0 or "".
  • 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%, and all other orders receive no discount. Reject negative totals and print the 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

js
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.

js
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.

js
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.

  1. Why does branch order matter in grading?
  2. Name five falsy values.
  3. When is a ternary clearer than if/else?
  4. Why should 0 often be checked explicitly?
  5. Which boundary values would you test for age >= 18?

Official References

References checked 2026-08-24.

Reader page: /javascript/lesson/047/control-flow-conditions-and-branching