Skip to content

JavaScript for Interactive Interfaces

Step 5 of 45

Values, Branches, and Loops

Introduction

This lesson opens the second chapter of the course and matches the JavaScript fundamentals of course 2 of the Meta Front-End Developer pathway ("Programming with JavaScript"). In course 1 you built static pages. From now on pages react to people: they validate a form, compute a price, and show different content for different data. All of that rests on four ideas: values, variables, decisions (branches), and repetition (loops). The pathway does not publish exam weights, so this course treats JavaScript as a heavily practiced domain and gives it more questions than most.

By the end you will be able to declare variables with the right keyword, predict what an expression evaluates to (including surprising coercions), write conditions and loops that behave correctly at their edges, and find a bug with a breakpoint instead of guessing. The practice scenario is original: repair the Harbor Workshops price calculator that glues numeric strings together instead of adding them.

Learning objectives

  1. Choose between const and let, and explain why var is avoided in new code.
  2. Name the primitive types and predict the result of typeof, comparison, and arithmetic on mixed types.
  3. Explain implicit type coercion and convert values explicitly and safely.
  4. Use ===, truthy/falsy rules, &&, ||, and ?? correctly.
  5. Write if/else, ternary, and switch branches, and for, for...of, while loops.
  6. Describe block scope and the temporal dead zone.
  7. Debug with console.log, breakpoints, and the call stack in browser developer tools.

1. Values and variables

A value is a piece of data. A variable is a named binding to a value. JavaScript has seven primitive types you need now:

Type Example Notes
string "Harbor" Text; immutable
number 12.5, -3, NaN, Infinity One floating-point type for integers and decimals
bigint 9007199254740993n Integers beyond the safe range; rarely needed in UI work
boolean true, false
undefined undefined A variable that was declared but never assigned
null null An intentional "no value"
symbol Symbol("id") Unique keys; you will rarely write one

Everything else (arrays, plain objects, functions, dates) is an object.

Declare variables with const or let:

const siteName = "Harbor Workshops"; // never reassigned
let seatsLeft = 12;                  // will change
seatsLeft = seatsLeft - 1;           // fine
siteName = "Other";                  // TypeError: Assignment to constant variable.

const means the binding cannot be reassigned, not that the value is frozen. A const object can still have its properties changed:

const workshop = { title: "Pottery", seats: 8 };
workshop.seats = 7;        // allowed: same object, new property value
// workshop = {};          // TypeError: the binding is constant

Rule of thumb: start with const; switch to let only when you truly reassign. This makes code easier to read, because a reader knows a const name always points at the same value.

var is the older keyword. It is function-scoped rather than block-scoped, is hoisted with the value undefined, and allows silent redeclaration. These traits cause bugs, so modern code and the React tooling in this course avoid it. You must still recognize it when you read older code.

Naming. Use camelCase for variables and functions (seatsLeft), PascalCase for classes and, later, React components (WorkshopCard), and UPPER_SNAKE_CASE for true constants (MAX_SEATS). Names are case-sensitive and cannot start with a digit.

Worked example: predicting typeof

Task. Predict the output of each line before running it.

console.log(typeof "5");        // ?
console.log(typeof 5);          // ?
console.log(typeof null);       // ?
console.log(typeof undefined);  // ?
console.log(typeof []);         // ?
console.log(typeof (() => 1));  // ?

Reasoning. A string literal is a string, a number literal is a number. typeof null returns "object"; this is a historical quirk that cannot be fixed without breaking the web, so to test for null you write value === null. typeof undefined is "undefined". An array is an object, so typeof [] is "object"; use Array.isArray(value) to detect arrays. Functions report "function".

Output: string, number, object, undefined, object, function.

2. Numbers, strings, and coercion

Numbers are floating point

All JavaScript numbers are 64-bit floating-point values. Some decimals cannot be represented exactly:

console.log(0.1 + 0.2);            // 0.30000000000000004
console.log((0.1 + 0.2).toFixed(2)); // "0.30" (a string!)

For money, the safest habit is to compute in the smallest unit (cents) as integers and divide for display. Note that toFixed returns a string, so it belongs at the very end, when you format output.

Useful values and operators: % (remainder), ** (power), Infinity (from 10 / 0), and NaN, "not a number", which is the result of a failed numeric operation such as Number("abc"). NaN is never equal to anything, even itself, so use Number.isNaN(value).

Strings

Strings use single quotes, double quotes, or backticks. Template literals (backticks) allow embedded expressions and multi-line text:

const total = 25;
console.log(`Total: ${total.toFixed(2)} EUR`); // Total: 25.00 EUR

Strings have a length property and methods such as toUpperCase(), trim(), includes(), slice(), and split(). They are immutable: methods return new strings.

Implicit coercion

JavaScript converts types automatically when an operator mixes them. This is called coercion and it is the number-one source of beginner bugs. The key rules:

  • + with at least one string operand concatenates.
  • -, *, / convert both sides to numbers.
console.log("5" + 3);   // "53"  (concatenation)
console.log("5" - 3);   // 2     (numeric)
console.log("5" * "2"); // 10
console.log(+"");       // 0
console.log(+"abc");    // NaN

Prefer explicit conversion so intent is visible:

Number("42");        // 42
Number("42px");      // NaN    (strict: the whole string must be numeric)
parseInt("42px", 10); // 42    (reads a prefix)
parseFloat("3.5kg"); // 3.5
String(42);          // "42"
Boolean("x");        // true

Always pass the radix 10 to parseInt. Use Number() when the whole string must be a valid number (form fields), and parseInt/parseFloat only when a prefix is acceptable. Remember that Number("") is 0, so an empty input silently becomes zero unless you check for it.

Expression Result Why
"5" + 3 "53" + prefers string concatenation
"5" - 3 2 - is numeric only
Number(null) 0 null converts to zero
Number(undefined) NaN undefined has no numeric meaning
Number(true) 1 booleans convert to 1/0

3. Comparison, truthiness, and logical operators

Equality

== compares after coercion; === compares without coercion and also checks the type. Use === and !== by default.

console.log(1 == "1");          // true
console.log(1 === "1");         // false
console.log(0 == "");           // true   (surprising)
console.log(null == undefined); // true
console.log(null === undefined);// false
console.log(NaN === NaN);       // false

A single exception many teams accept is value == null, which is true for both null and undefined. In this course we use === everywhere and write the two checks explicitly when needed.

Objects are compared by reference: two separate {} are never equal, even if their contents match.

Truthy and falsy

In a condition, every value is treated as either true or false. There are exactly these falsy values: false, 0, -0, 0n, "", null, undefined, and NaN. Everything else is truthy, including "0", "false", [], and {}.

Boolean("");   // false
Boolean("0");  // true
Boolean([]);   // true
Boolean({});   // true
Boolean(0);    // false

&&, ||, ??

Logical operators return one of their operands, not necessarily a boolean:

console.log("a" || "b");  // "a"   first truthy
console.log("" || "b");   // "b"
console.log(1 && "x");    // "x"   last value if all truthy
console.log(0 && "x");    // 0     first falsy
console.log(0 || 10);     // 10
console.log(0 ?? 10);     // 0     ?? only replaces null/undefined
console.log(null ?? "b"); // "b"

The nullish coalescing operator ?? is the right tool for defaults when 0 or "" are valid values. For instance, a workshop with 0 seats left must show 0, not a default of 10:

const shown = seatsLeft ?? 10;  // keeps 0
const wrong = seatsLeft || 10;  // turns 0 into 10: a bug

Optional chaining ?. safely reads nested properties: workshop?.host?.name gives undefined instead of throwing if host is missing.

4. Branches

if / else

function seatMessage(seats) {
  if (seats === 0) {
    return "Sold out";
  } else if (seats < 5) {
    return `Only ${seats} left`;
  }
  return "Seats available";
}

Prefer early returns over deep nesting. Always use braces, even for one-line bodies; omitting them is a classic source of bugs when someone adds a second line.

Ternary

For a simple value choice, use the conditional operator:

const label = seats === 0 ? "Full" : seats < 5 ? "Few" : "Open";

Nest at most once; beyond that use if.

switch

switch compares with === and falls through to the next case unless you break or return:

switch (2) {
  case 1: console.log("one");
  case 2: console.log("two");    // runs
  case 3: console.log("three");  // also runs: no break
    break;
  default: console.log("other");
}
// Output: two, three

Use switch for one variable compared to several constants; use if for ranges.

5. Loops

Loop Use when Example
for You need an index or a known count for (let i = 0; i < 3; i++)
for...of You want each value of an array or string for (const p of prices)
for...in You want object keys (avoid on arrays) for (const k in obj)
while Repeat until a condition changes while (n > 0)
do...while Run at least once do { } while (cond)

break leaves the loop; continue skips to the next iteration:

for (let i = 0; i < 5; i++) {
  if (i === 1) continue; // skip 1
  if (i === 4) break;    // stop at 4
  console.log(i);
}
// 0, 2, 3

for...in over an array yields string indices ("0", "1"), which surprises people; use for...of for arrays. Beware of infinite loops: make sure the loop variable moves toward the end condition. If a page freezes, an infinite loop is a prime suspect.

Worked example: summing prices

Task. The price list arrives from a form as strings. Compute the total.

const prices = ["12.50", "8", "3.25"];
let total = 0;
for (const p of prices) {
  total += p;
}
console.log(total); // ?

Reasoning, step by step.

  1. total starts as the number 0.
  2. First pass: 0 + "12.50". One operand is a string, so + concatenates: "012.50".
  3. Second pass: "012.50" + "8" gives "012.508".
  4. Third pass: the result is "012.5083.25".

Output: 012.5083.25, a string, not 23.75.

Fix. Convert each price, and work in cents to avoid float noise:

let cents = 0;
for (const p of prices) {
  cents += Math.round(Number(p) * 100);
}
console.log(cents / 100); // 23.75

Why other fixes are weaker. Starting total as parseFloat(prices[0]) only patches the first iteration. Wrapping the final result in Number() is too late: the garbage string "012.5083.25" converts to NaN. Converting each input at the boundary is the correct place.

6. Scope and the temporal dead zone

Scope is where a name is visible. let and const are block-scoped: they exist only inside the nearest { }. var ignores blocks and is function-scoped.

The classic demonstration involves closures in loops:

var fns = [];
for (var i = 0; i < 3; i++) {
  fns.push(() => i);
}
console.log(fns.map((f) => f())); // [3, 3, 3]

const fl = [];
for (let j = 0; j < 3; j++) {
  fl.push(() => j);
}
console.log(fl.map((f) => f())); // [0, 1, 2]

With var there is a single i shared by all functions, and by the time they run it equals 3. With let, each iteration gets its own j.

Hoisting. Declarations are processed before code runs. Function declarations are fully hoisted, so you can call them above their definition. var variables are hoisted as undefined. let and const are hoisted but not initialized: reading them before their line throws a ReferenceError. That interval is the temporal dead zone (TDZ).

console.log(typeof hoisted, typeof hv); // function undefined
var hv = 1;
function hoisted() {}

try {
  console.log(tdz);
  let tdz = 1;
} catch (e) {
  console.log(e.name, e.message);
  // ReferenceError Cannot access 'tdz' before initialization
}

7. Debugging

Guessing wastes time; observe instead.

  1. Reproduce the bug with the smallest input.
  2. console.log values and their types: console.log(typeof p, p). Use console.table(array) for lists and console.error for failures.
  3. Breakpoints. In the Sources panel, click a line number; reload; execution pauses there. Use Step over (next line), Step into (enter a call), Step out. The Scope pane shows current variables and the Call stack pane shows how you got here.
  4. Conditional breakpoints pause only when an expression is true (i === 2), which is ideal for loops.
  5. Read the error. TypeError: Cannot read properties of undefined (reading 'seats') means something before .seats was undefined; the line number in the stack trace points at the culprit.
  6. Insert a debugger; statement as a code-based breakpoint, but remove it before committing.
Error Typical cause
ReferenceError: x is not defined Typo, wrong scope, or TDZ
TypeError: x is not a function Called something that is not a function
TypeError: Assignment to constant variable Reassigned a const
SyntaxError Missing bracket or quote; the file will not run at all

8. Pitfalls and exam tips

  • Pitfall: "5" + 3 is "53". Exam tip: when a question mixes + with a string, expect concatenation.
  • Pitfall: treating const as "immutable value". It only prevents reassignment.
  • Pitfall: || for defaults when 0 or "" is valid. Use ??.
  • Pitfall: typeof null === "object"; arrays also report "object".
  • Pitfall: forgetting break in switch, causing fall-through.
  • Pitfall: for...in on arrays gives string keys.
  • Exam tip: to predict output, evaluate from the inside out and write each intermediate value, as in the worked example.
  • Exam tip: when an option says "use == so types match", prefer the option that converts explicitly and compares with ===.

Mini case study: the booking total that says NaN

Situation. On the Harbor booking page a visitor selects the number of seats in a text field and sees the total. The code is:

const pricePerSeat = 18;
const seatsInput = document.querySelector("#seats").value; // "3"
let discount;
if (seatsInput > 4) {
  discount = 0.1;
}
const total = seatsInput * pricePerSeat * (1 - discount);
console.log(total);

For 3 seats the page shows NaN. For 5 seats it shows 81.

Questions.

  1. Why does 3 seats give NaN?
  2. Why does the comparison seatsInput > 4 work although seatsInput is a string?
  3. Fix the code.

Commented solution.

  1. discount is declared with no value, so it is undefined when the condition is false. 1 - undefined is NaN, and NaN poisons the whole multiplication.
  2. > coerces the string to a number, so "5" > 4 is true. It works by accident of coercion, which hides the real problem: the input is still a string and an empty or invalid entry gives NaN or 0 silently.
  3. Convert and validate at the boundary and give discount a default:
const pricePerSeat = 18;
const seats = Number(document.querySelector("#seats").value);

if (!Number.isInteger(seats) || seats < 1) {
  console.error("Enter a whole number of seats");
} else {
  const discount = seats > 4 ? 0.1 : 0;
  const total = seats * pricePerSeat * (1 - discount);
  console.log(total.toFixed(2)); // "54.00" for 3, "81.00" for 5
}

Number("") is 0, which fails the seats < 1 check, so an empty field is rejected too. The debugging path would be to set a breakpoint on the total line and read discount in the Scope pane.

Key terms

  • Primitive: an immutable basic value such as a number or string.
  • Binding: the link between a name and a value.
  • Coercion: automatic or explicit conversion between types.
  • Truthy / falsy: how a value behaves in a condition.
  • Nullish: null or undefined.
  • Block scope: visibility limited to the nearest braces.
  • Temporal dead zone: the period in which a let/const exists but cannot be read.
  • Breakpoint: a marker that pauses execution for inspection.

Summary checklist

  • I use const by default and let when I reassign; I avoid var.
  • I can list the primitive types and know typeof null is "object".
  • I predict "5" + 3 and "5" - 3 correctly.
  • I convert user input explicitly with Number() and validate it.
  • I use ===, and ?? for defaults where 0 is valid.
  • I know the eight falsy values.
  • I choose the right loop and use break/continue deliberately.
  • I can explain block scope, hoisting, and the temporal dead zone.
  • I can pause code with a breakpoint and read the Scope and Call stack panes.

Official sources