JavaScript Values, Type Coercion, and Control Flow
Introduction
JavaScript values, type coercion, and control flow turn a static webpage into an interactive interface. This lesson opens chapter 2 and aligns with Meta’s "Programming with JavaScript" course. 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 Meta Professional Certificate is a course pathway with assessments and projects; these original exercises practise the skills without claiming an official exam weighting.
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
- Choose between
constandlet, and explain whyvaris avoided in new code. - Name the primitive types and predict the result of
typeof, comparison, and arithmetic on mixed types. - Explain implicit type coercion and convert values explicitly and safely.
- Use
===, truthy/falsy rules,&&,||, and??correctly. - Write
if/else, ternary, andswitchbranches, andfor,for...of,whileloops. - Describe block scope and the temporal dead zone.
- 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 constantRule 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 scoped to its enclosing function, or to the script/module scope when outside a function, rather than an ordinary block. Its binding is initialized to undefined before execution reaches the declaration and it permits redeclaration in many cases. 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
Values of the number type use 64-bit binary floating point. bigint is a separate integer type; it is not a floating-point number. 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 this euro-price example, compute in cents using safe integers and divide for display. Integers remain exact only within Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER; validate each amount and the accumulated total. Other currencies can have different minor-unit rules. 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 compares unequal to itself with both == and ===; use Number.isNaN(value) to detect it. Object.is(NaN, NaN) is a separate comparison that returns true.
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 EURStrings have a length property and methods such as toUpperCase(), trim(), includes(), slice(), and split(). Strings are immutable: these methods do not change the original. The result depends on the method: trim() returns a string, includes() a boolean, and split() an array.
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. For the number and string examples here:
+with at least one string operand concatenates.-,*,/perform numeric operations. They also support BigInt operands, but mixing a BigInt with a Number in arithmetic throwsTypeError.
console.log("5" + 3); // "53" (concatenation)
console.log("5" - 3); // 2 (numeric)
console.log("5" * "2"); // 10
console.log(+""); // 0
console.log(+"abc"); // NaNPrefer 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"); // trueAlways 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("") and whitespace-only strings become 0; check for blank input first. Use Number.isFinite() when a finite quantity is required, and validate the application's range and accepted syntax.
| 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); // falseA 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. The primitive falsy values are: false, 0, -0, 0n, "", null, undefined, and NaN. Ordinary objects are truthy, including [] and {}, as are the non-empty strings "0" and "false". Browsers retain one legacy exception, document.all, a falsy host object that also compares loosely equal to null. It is not a pattern to use in application code.
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 bugOptional chaining ?. safely reads nested properties: workshop?.host?.name gives undefined if a declared workshop is nullish or its host is missing. It does not make an undeclared root identifier safe.
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, threeUse 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, 3for...in visits enumerable string-keyed properties, including inherited ones; on arrays it can visit more than the index keys ("0", "1"). Use for...of for array values, or Object.keys()/Object.hasOwn() when you need own object properties. 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.
For related practice, study validating JavaScript inputs and handling errors.
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.
totalstarts as the number0.- First pass:
0 + "12.50". One operand is a string, so+concatenates:"012.50". - Second pass:
"012.50" + "8"gives"012.508". - Third pass: the result is
"012.5083.25".
Output: 012.5083.25, a string, not 23.75.
Fix. Parse this exercise’s non-negative decimal euro prices into cents. Accept at most two decimal places; do not silently round arbitrary input such as "1.005", because Math.round(Number(text) * 100) is not a universal decimal-money parser:
function priceInCents(text) {
const match = /^(\d+)(?:\.(\d{1,2}))?$/.exec(text.trim());
if (!match) throw new TypeError("Use a non-negative price with up to two decimals");
const cents = Number(match[1]) * 100 + Number((match[2] ?? "").padEnd(2, "0"));
if (!Number.isSafeInteger(cents)) throw new RangeError("Price is too large");
return cents;
}
let cents = 0;
for (const p of prices) {
cents += priceInCents(p);
if (!Number.isSafeInteger(cents)) throw new RangeError("Total is too large");
}
console.log(cents / 100); // 23.75Why 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 declarations in a block are visible within that lexical block, including nested scopes that do not shadow them. A for (let ...) header also introduces bindings. var does not gain a new scope from an ordinary block.
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). Even typeof throws when applied to a binding in its TDZ; this differs from typeof on a completely undeclared name.
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; the exact message varies by engine
}For related practice, study browser developer tools and HTTP debugging.
7. Debugging
Guessing wastes time; observe instead.
- Reproduce the bug with the smallest input.
console.logvalues and their types:console.log(typeof p, p). Useconsole.table(array)for lists andconsole.errorfor failures.- 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.
- Conditional breakpoints pause only when an expression is true (
i === 2), which is ideal for loops. - Read the error.
TypeError: Cannot read properties of undefined (reading 'seats')means something before.seatswasundefined; the line number in the stack trace points at the culprit. - 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 study tips
- Pitfall:
"5" + 3is"53". Study tip: when a question mixes+with a string, expect concatenation. - Pitfall: treating
constas "immutable value". It only prevents reassignment. - Pitfall:
||for defaults when0or""is valid. Use??. - Pitfall:
typeof null === "object"; arrays also report"object". - Pitfall: forgetting
breakinswitch, causing fall-through. - Pitfall:
for...inon arrays gives string keys. - Study tip: to predict output, evaluate from the inside out and write each intermediate value, as in the worked example.
- Study 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.
- Why does 3 seats give
NaN? - Why does the comparison
seatsInput > 4work althoughseatsInputis a string? - Fix the code.
Commented solution.
discountis declared with no value, so it isundefinedwhen the condition is false.1 - undefinedisNaN, andNaNpoisons the whole multiplication.>coerces the string to a number, so"5" > 4istrue. It works by accident of coercion, which hides the real problem: the input is still a string and an empty or invalid entry givesNaNor0silently.- Convert and validate at the boundary and give
discounta default:
const pricePerSeat = 18;
const seats = Number(document.querySelector("#seats").value);
if (!Number.isSafeInteger(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:
nullorundefined. - Block scope: visibility limited to the nearest braces.
- Temporal dead zone: the period in which a
let/constexists but cannot be read. - Breakpoint: a marker that pauses execution for inspection.
Summary checklist
- I use
constby default andletwhen I reassign; I avoidvar. - I can list the primitive types and know
typeof nullis"object". - I predict
"5" + 3and"5" - 3correctly. - I convert user input explicitly with
Number()and validate it. - I use
===, and??for defaults where0is valid. - I know the primitive falsy values and recognize the legacy browser exception.
- I choose the right loop and use
break/continuedeliberately. - 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
- Meta Front-End Developer Professional Certificate (opens in a new tab)
- Course 2: Programming with JavaScript (opens in a new tab)
- MDN: JavaScript Guide, Grammar and types (opens in a new tab)
- MDN: Equality comparisons and sameness (opens in a new tab)
- MDN: let (opens in a new tab)
- Chrome DevTools: Debug JavaScript (opens in a new tab)
- MDN: Number and safe integers (opens in a new tab)
- MDN: Falsy values and the browser legacy exception (opens in a new tab)
