Skip to content

JavaScript Foundations: Values, Functions, Async, and Tests

Step 5 of 45

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

  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:

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

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

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

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

JavaScript
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(). 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 throws TypeError.
JavaScript
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:

JavaScript
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("") 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.

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

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

&&, ||, ??

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

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

JavaScript
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 if a declared workshop is nullish or its host is missing. It does not make an undeclared root identifier safe.

4. Branches

if / else

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

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

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

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

JavaScript
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. 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:

JavaScript
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.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 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:

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

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

  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 study tips

  • Pitfall: "5" + 3 is "53". Study 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.
  • 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:

JavaScript
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:
JavaScript
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: 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 primitive falsy values and recognize the legacy browser exception.
  • 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