← Front-End Developer — all chapters  ·  Kelick Tech Hub

Chapter 16 — JavaScript Foundations: Values and Variables

Section 1 — What a value is

A value is a single piece of data your program works with: a number, a piece of text, or a true-or-false answer. That definition sounds too simple to be worth a section, but it is the foundation of everything that follows, because every program, no matter how large, is a machine for taking values in, transforming them, and producing values out. A page that shows a price is displaying a value. A form that greets a user by name is displaying a value the user supplied. A game that tracks a score is updating a value every time something happens. A shopping cart is a collection of values with rules about how they combine. When you strip away frameworks and libraries, programming is the manipulation of values, and the rest of this book is about the increasingly powerful ways to store, combine, transform and act on them.

Values come in kinds, called types, and the type of a value determines what you can do with it. You can add two numbers, but adding two pieces of text means something different — joining them — and adding a number to a piece of text does something else again, which is a frequent source of surprises that this chapter will explain properly. You can ask whether a number is greater than another number, but asking whether one piece of text is greater than another is a less obvious operation, even though JavaScript allows it. Types are therefore not pedantry; they are the rules of what each value can do, and much of debugging is discovering that a value is not the type you assumed it was.

Variables are the other half of this section's idea, and they are best understood as named places to keep values so you can use them later. Without variables, a program could only work with values written literally in its code, which would mean the same result every time it ran. With variables, a program can accept a user's name and use it in a greeting, accept a price and calculate tax on it, accept a score and update it as a game progresses. The variable is what makes a program general rather than specific: the code says "take whatever name is in this box and greet that person", and the box can hold any name. That generality is the whole point of programming, and it starts with the simple act of naming a value.

There is a useful mental image for all of this. Think of a value as a thing — a number written on a slip of paper, a word on a card — and a variable as a labelled box that holds one such thing at a time. The label is the variable's name, and it never changes. The contents can change, if the variable allows it, which is the subject of the sections on let and const. Code reads the box by its label, puts new things in the box, and takes things out to work with. When you read code later, tracking what is in each box at each moment is the main skill, and this chapter gives you the vocabulary for doing it: values, types, variables, assignment, and the operators that transform one value into another.

Check: What is a value, in one sentence?

Section 2 — Numbers

Numbers in JavaScript are written plainly, with no quotes: 42, 3.14, -7, 0. They are used for counting, measuring and calculating, and they cover everything from a user's age to a product's price to the number of pixels an element should be wide. JavaScript deliberately has one number type for both whole numbers and decimals, unlike some languages that distinguish integers from floating-point numbers with separate types and separate rules. That simplicity is a gift to beginners: you do not need to decide which kind of number you have, and arithmetic works the same way for both. The cost of that simplicity is a small set of quirks around decimal precision, which Section 31 explains, but for everyday arithmetic the single number type behaves exactly as school mathematics would lead you to expect.

let age = 30;
let price = 9.99;

Writing a number is the easy part; knowing what counts as a number is where beginners occasionally stumble. A number with quotes around it — "42" — is not a number at all, but a string that happens to look like one, and it behaves differently in calculations, as Section 44 explains. A number written with commas, like 1,000, is not valid JavaScript at all, because the comma means something else in the language. A number written with a leading zero or in an unusual format may be interpreted in ways you did not intend. The rule is simple: write numbers as plain digits, with a single decimal point if needed, and a minus sign in front for negatives, and JavaScript will understand you.

Numbers also have a range and a precision limit, though you will rarely hit them in ordinary front-end work. Very large numbers lose exactness beyond about nine quadrillion, which is why Section 46 introduces BigInt for the rare cases where exact huge integers matter. Very small decimal differences can also be lost, which is why money is best handled in whole minor units, as Section 31 explains. For the overwhelming majority of front-end tasks — counts, prices, sizes, scores, ages, quantities — ordinary numbers are exactly right, and you should use them without worrying about their limits until a specific problem tells you otherwise.

Finally, numbers are the values most often produced by calculation, and calculation is where programs earn their keep. A total is a sum of numbers. A discount is a multiplication. An average is a sum divided by a count. A page position is a number of pixels. Learning to see the numbers inside a problem — what is counted, what is measured, what is calculated from what — is a large part of learning to program, and it starts with recognising that numbers in code are not decorations. They are the quantities your program reasons about, and naming them well, as Section 10 explains, is part of making that reasoning clear to the next person who reads your code.

Check: How do you write the number forty-two in JavaScript?

Section 3 — Strings

A string is text wrapped in quotes, and it is how JavaScript represents words: names, messages, addresses, labels, sentences — any sequence of characters your page needs to show, store or manipulate. You can use double quotes ("hello"), single quotes ('hello'), or backticks (hello), and for most purposes they are interchangeable. The quotes are not part of the text; they are markers that tell JavaScript where the text starts and ends, and that what lies between them should be treated as data rather than as instructions. Without quotes, hello would be read as a variable name, and the browser would complain that no such variable exists. With quotes, it is simply the word hello, available to display or to work with.

let greeting = "Hello, world!";

Strings are everywhere in front-end work, far more than beginners expect. The text on a button is a string. The address a user types into a form arrives as a string. The title of a page is a string. The content fetched from a server usually arrives as strings, even when it represents numbers or dates, which is why converting between strings and numbers is a recurring task. A program that seems to be about numbers — a calculator, a shopping cart — is usually also about strings, because its results must be displayed as text, and its inputs arrive as text from form fields. Learning to handle strings fluently is therefore not a side topic; it is central to building pages that communicate with people.

A string can be empty — "" — which is a real string with no characters in it, and it is different from a missing value, as Section 5 explains. A string can contain digits, spaces, punctuation, and characters from any writing system. A string can be long, holding a whole paragraph, or short, holding a single letter. What a string cannot do is change in place, a property called immutability that Section 30 explains: operations on strings produce new strings rather than altering the original. That property is worth knowing early, because it explains why a method that seems to change a string appears to do nothing unless you store its result.

Choosing between quote styles is mostly a matter of consistency and convenience. Double quotes are the most common in many codebases. Single quotes are preferred in others, and they make it easy to include double quotes inside a string without escaping. Backticks create template literals, which are a special kind of string with extra powers — embedding values and spanning lines — and Section 17 covers them in full. The practical rule is to pick one style for ordinary strings, use it consistently, and reach for the other styles when a specific string needs them: a string containing a quote, or a string containing a value. Consistency makes code easier to scan, and scanning is what you will spend most of your reading time doing.

Check: Why does text need quotes around it in JavaScript?

Section 4 — Booleans

A boolean is one of two values: true or false, written without quotes, in lowercase. Booleans are named after George Boole, a nineteenth-century mathematician who developed a system of logic based on true and false, and they are the values behind every decision a program makes. Is the user logged in? Is the form complete? Is the light on? Has the game ended? Each of these questions has a yes-or-no answer, and in code that answer is a boolean. Where numbers measure and strings describe, booleans decide, and decisions are what turn a static page into an interactive one.

let loggedIn = true;
let finished = false;

Booleans are produced in two main ways. The first is direct assignment, as in the example: a variable is set to true or false because the program knows the answer at that point. The second, far more common, is comparison: an expression like age >= 18 evaluates to a boolean, true if the comparison holds and false if it does not. That is why comparisons and booleans belong together in your mind. A comparison is a question, and a boolean is its answer. Storing that answer in a well-named variable — canProceed, isAdult, hasItems — makes the code that follows read almost like English: if the user can proceed, show the next step. Naming booleans as questions or states, often starting with is, has, or can, is a convention that pays off every time someone reads your code.

Booleans are also the values that control flow. An if statement runs its block when its condition is true and skips it when it is false. A loop repeats while its condition is true and stops when it becomes false. A logical operator combines booleans into new booleans, as Section 22 explains. None of these mechanisms work with numbers or strings directly; they work with booleans, or with values that JavaScript converts to booleans, as Section 23 explains. Understanding that chain — comparison produces a boolean, logic combines booleans, control flow consumes booleans — is understanding a large part of how programs make decisions, and it is a chain you will use in every program you write from here on.

A common beginner mistake is quoting booleans: writing "true" instead of true. The quoted version is a string, not a boolean, and while a non-empty string is truthy in a boolean context, it is not equal to true under strict comparison, which produces confusing bugs. Another mistake is overcomplicating boolean logic: writing if (loggedIn === true) when if (loggedIn) says the same thing more plainly. Booleans are simple values, and code that treats them simply is easier to read and harder to get wrong. When a value can only be true or false, let it be a boolean, name it as a question, and let the rest of the program ask it.

Check: What two values can a boolean hold?

Section 5 — undefined and null

Two values in JavaScript both mean, roughly, "nothing here", and confusing them is a rite of passage. The first is undefined, which means "no value has been given yet". It is the value of a variable that has been declared but not assigned, the value of a function that returns nothing, and the value of an object property that does not exist. Undefined is, in a sense, the language's default: when JavaScript needs a value and none has been provided, it supplies undefined. You will rarely assign undefined deliberately, because it usually arrives on its own, as a sign that something has not happened yet.

let nickname; // undefined
let middleName = null; // deliberately empty

The second is null, which means "deliberately empty". A programmer sets a variable to null to say, explicitly, that there is intentionally nothing here right now. A user with no middle name might have middleName set to null, not because the program forgot to ask, but because the answer is genuinely "none". A search that found no results might return null to say "I looked, and there is nothing". The difference is one of intent: undefined says "not yet" or "never provided", while null says "provided, and the provision is emptiness". That distinction is subtle, and many programs treat the two interchangeably, but when you are debugging, it matters: undefined points you towards a missing assignment or a missing return, while null points you towards a deliberate decision that produced emptiness.

In practice, you will meet undefined far more often than null, because it is the language's way of flagging gaps. A variable logged as undefined tells you its assignment did not run, or ran on a different path. A property read as undefined tells you the object does not have that property, perhaps because of a misspelling or because the data is shaped differently from your expectation. A function result of undefined tells you the function did not return anything, perhaps because it has no return statement on the path that ran. Each of these is a lead, and each is more informative than a generic "nothing" would be, precisely because undefined is specific: it means the language, not the programmer, supplied the emptiness.

Null, by contrast, is a tool you choose. Use it when emptiness is a meaningful state that your program must distinguish from "not yet known". A form field the user has deliberately left blank might be null, while a field the program has not yet examined might be undefined, and code that cares about the difference can act accordingly. Not every program needs that distinction, and many codebases use only one of the two values consistently. But knowing both, and knowing which one you are looking at when a log prints it, will save you from a common debugging confusion: treating a deliberate null as a bug, or treating an accidental undefined as intentional. Ask, for any empty value, whether a human chose it or the language supplied it, and you will usually know where to look next.

Check: What is the difference between undefined and null?

Section 6 — Checking a type with typeof

When a value surprises you — a calculation gives a string instead of a number, a variable holds undefined instead of an object, a comparison behaves unexpectedly — the first question to ask is "what type is this value, really?" The typeof operator answers that question. Put typeof in front of a value or a variable, and it returns a string naming the type: "number" for numbers, "string" for strings, "boolean" for booleans, "undefined" for undefined, "function" for functions, and "object" for objects and, famously, for null. It is a small operator with a large debugging role, because so many bugs are type bugs in disguise.

typeof 42;      // "number"
typeof "hello"; // "string"

Using typeof is straightforward, but using it well involves knowing when to reach for it. Reach for it when a value's behaviour does not match your expectation: a plus operation that concatenates instead of adding, a comparison that returns false when it should return true, a method call that fails because the value does not have that method. In each case, logging typeof alongside the value turns a vague surprise into a precise diagnosis. The value is not wrong; it is a different type from the one you assumed, and once you know that, the question becomes where the type changed, which is a much more tractable question than "why is this broken?"

There are quirks to know, and this book does not hide them. Typeof null returns "object", a leftover from JavaScript's earliest implementation that has never been fixed because changing it would break existing code. Section 41 returns to this quirk and explains how to test for null properly. Typeof an array also returns "object", because arrays are a kind of object, and distinguishing arrays from other objects requires a different test, which a later chapter covers. Typeof a function returns "function", which is convenient, though functions are also technically objects. These quirks do not make typeof unreliable; they make it a first check rather than a final verdict. When typeof says "object", you know the value is not a primitive, and you then investigate further with more specific tests.

A good habit is to pair typeof with console.log during debugging: log both the value and its type, so the Console shows you not just what a variable holds but what kind of thing it holds. That pairing catches the most common type surprise of all, the string that looks like a number, because the value "42" and the value 42 look identical in some Console displays but have different types, and typeof exposes the difference instantly. As your programs grow, type awareness becomes second nature: you will start to think of every variable as holding not just a value but a value of a type, and you will check the type whenever behaviour diverges from expectation. Typeof is the tool that makes that thinking concrete.

Check: What does typeof "hello" return?

Section 7 — Declaring a variable with let

You create a variable with let followed by a name, then optionally an equals sign and a starting value. The line let score = 0 does three things at once: it brings a variable called score into existence, it gives that variable the starting value 0, and it announces to anyone reading the code that a value called score will be used from here on. The name is how you refer to the value later: wherever score appears in the code that follows, it means the current contents of that variable's box. Think of let as labelling a box: the label is the name, the value sits inside, and the code that follows can read the label to find the value.

let score = 0;

The starting value is optional but almost always worth giving. A variable declared without one, like let nickname;, starts life holding undefined, which is correct in the sense that no value has been provided yet, but it is also a source of bugs if the variable is read before it is assigned. Giving a sensible starting value — 0 for a counter, "" for text being built up, false for a flag — makes the variable's initial state explicit and removes a whole class of "why is this undefined?" puzzles. When you genuinely do not have a starting value yet, because the value will come from the user or from a calculation later, declaring without one is honest, but it carries an obligation: assign before you read.

Let is the declaration for values that change. A score goes up. A total accumulates. A current page changes as the user navigates. Those are let variables, because their contents are expected to move over time. The companion declaration, const, is for values that do not change, and Section 8 covers it in full. The choice between them is not cosmetic; it is a statement of intent that the language enforces. Declaring a value with let tells every reader, including your future self, "this value moves, watch it". Declaring it with const tells them "this value stays, rely on it". That communication is part of why the let-versus-const choice matters more than it first appears.

There is one more detail that belongs in a first encounter with let: a variable declared with let cannot be declared again with let in the same scope. Writing let score = 0 twice in the same block is an error, because the language refuses to create two boxes with the same label in the same place. To change a let variable's value, you assign to it without let, as Section 13 explains. That rule prevents a common confusion in which a second declaration silently creates a second variable that shadows the first, and it means that when you see let, you know you are looking at a variable's birth, not at a reassignment. Birth happens once; changes happen by assignment.

Check: What does the line let score = 0 do?

Section 8 — Constants with const

Const declares a variable whose value cannot be reassigned. The word comes from "constant", and the promise is simple: once a const variable has its value, that value is fixed for the rest of its life. If code tries to assign a new value to it, JavaScript throws an error immediately, naming the assignment as the problem. That error is a feature, not an annoyance, because it catches a mistake at the moment it happens: code that was not supposed to change a value tried to, and the language stopped it before the wrong value could propagate through the program and cause a confusing failure elsewhere.

const siteName = "Kelick Tech Hub";
siteName = "Other"; // Error: Assignment to constant variable.

Use const by default, and reach for let only when you can point to a place where the value must change. That rule, stated in Section 45 as well, is worth repeating because it is the single most useful variable habit a beginner can form. Most values in a program do not change: a site's name, a tax rate, a user's name once it is known, a configuration value, a reference to an element on the page. Declaring those with const makes the program's intentions visible and protects them from accidental reassignment. When a reader sees const, they can rely on the value. When they see let, they know to watch for changes. A codebase that follows this rule is easier to reason about, because the number of moving parts is honestly labelled.

There is a subtlety that confuses many learners, and it is better met early than late. Const prevents reassignment of the variable, not mutation of the value it holds, when that value is an object or an array. A const array can have items added to it; a const object can have its properties changed. What cannot happen is pointing the variable at a different array or object altogether. The box's label is fixed, and the box always points at the same thing, but the thing itself may change internally. This distinction matters more in later chapters, when objects and arrays become central, but the seed of it belongs here: const is about the binding between name and value, not about freezing the value itself. For numbers, strings and booleans — the values this chapter focuses on — the distinction does not arise, because those values are immutable: a const number cannot be changed in any way.

The practical upshot is a workflow. When you introduce a new variable, ask: will this value change? If the answer is no, or if you are unsure, use const. If the answer is yes, and you can say where and why it changes, use let. If you later discover that a const value does need to change, changing the declaration to let is a small, deliberate edit that documents a change in your understanding. That workflow turns variable declaration from a coin toss into a design decision, and design decisions, made consistently, are what make code readable.

Check: What happens if you try to reassign a const variable?

Section 9 — var, the old way

Before let and const existed, JavaScript variables were declared with var, and you will still meet var in old code, old tutorials, and answers to questions online. Understanding what var does, and why modern code avoids it, is useful even though you should not write it in new programs. Var declares a variable much like let does, but with two behaviours that cause subtle bugs, both stemming from the fact that var was designed in JavaScript's earliest days, before the language's scoping rules were fully thought through.

The first difference is scope. A var variable declared inside a block — inside an if statement, or inside a loop — is visible outside that block, as if it had been declared at the level of the enclosing function. That means a variable you thought was confined to a small section of code can leak out and be read or changed elsewhere, producing action at a distance that is hard to trace. Let and const, by contrast, are block-scoped: a variable declared inside a block stays inside that block, which matches the intuition most developers have about where a variable lives. The chapter on scope explores this in depth, but the headline is that block scoping is safer because it limits where a variable can be touched.

The second difference is redeclaration. Var allows the same name to be declared repeatedly in the same scope without an error, silently reusing the existing variable. That sounds harmless, but it hides mistakes: a second declaration that was meant to be a new variable, or a copy-pasted block that redeclares a name, will quietly share state with the first declaration, and the resulting bugs are difficult to spot because no error is ever thrown. Let and const refuse redeclaration in the same scope, turning that class of mistake into an immediate, visible error. An error that appears at the moment of the mistake is worth far more than silent misbehaviour discovered weeks later.

There is also hoisting, the behaviour where var declarations are moved, conceptually, to the top of their scope, so a var variable can be used before its declaration line without an error, holding undefined until the assignment runs. That behaviour, combined with block leakage and silent redeclaration, made var a frequent source of confusion, and it is the main reason the language's designers introduced let and const. When you read old code containing var, translate it mentally into let for understanding, but be alert for the differences: a var inside a block may be used outside it, and a var used before its line may hold undefined rather than throwing an error. When you write new code, use let and const, and treat var as a historical artefact you can recognise but need not use.

Check: Why should new code use let and const instead of var?

Section 10 — Naming variables

A variable's name is the handle by which every reader, including you in six months, will grasp what it holds. Names must follow the language's rules: they must start with a letter, an underscore, or a dollar sign, and they may contain letters, digits, underscores and dollar signs after that. They cannot contain spaces, they cannot start with a digit, and they cannot be a reserved word that the language already owns, like let or function. Within those rules, the choice of name is yours, and it is one of the most consequential small decisions in programming, because a good name makes code self-explanatory and a bad name makes it a puzzle.

let userName = "Ada";
let totalPrice = 25;

The principle is simple: a name should describe the value it holds, specifically enough to distinguish it from its neighbours. UserName beats name when a page also deals with product names and file names. TotalPrice beats total when a page also has a subtotal and a tax amount. A single letter like x is acceptable only when its meaning is obvious from immediate context, such as a coordinate in a tiny mathematical expression, and even there a fuller name is often clearer. The test to apply is whether a stranger reading the line could guess what the variable holds without reading the surrounding code. If they cannot, the name needs work.

There are conventions that help names do their job. Multi-word names use camelCase, as Section 11 explains: first word lowercase, later words capitalised, no separators. Boolean names read as questions or states — isLoggedIn, hasItems, canProceed — so that the code using them reads like a sentence. Names that hold collections are often plural — users, items, prices — signalling that the value is many things rather than one. Constants that are truly fixed configuration are sometimes written in capitals with underscores, like MAX_ATTEMPTS, though that convention varies between codebases. Following the conventions of the codebase you are in matters more than any single convention, because consistency lets readers predict names before they see them.

Bad names have costs that are easy to underestimate. A misleading name — calling a variable total when it actually holds a count — sends every reader down the wrong path, and the confusion compounds as the variable is used in more places. A vague name — data, value, thing — forces every reader to investigate what the variable actually holds, which is work that a precise name would have done for free. A name that is too clever, an abbreviation only its author understands, excludes future readers. Naming well is not vanity; it is the cheapest form of documentation, because the name is read every time the variable is used, while a comment is read only if someone thinks to look. Spend the extra seconds on a good name. They repay themselves many times over.

Check: Which of these names is invalid: userName, 2ndPlace, _total?

Section 11 — camelCase naming

JavaScript convention writes multi-word variable names in camelCase: the first word in lowercase, each subsequent word capitalised, with no spaces, hyphens or underscores between them. TotalPrice, firstName, isLoggedIn, shoppingCartTotal — each reads as a single word with humps in the middle, like a camel's back, which is where the name comes from. The convention is not enforced by the language; JavaScript will happily accept totalprice, total_price, or TotalPrice as variable names. It is enforced by the community, which is a stronger force in practice, because code is read by people who expect the convention and who will stumble, however slightly, over names that break it.

The value of a shared convention is consistency across codebases. When every JavaScript developer writes multi-word names the same way, a reader moving from one project to another can predict names before seeing them, can search for a variable with confidence about its spelling, and can distinguish, at a glance, variables from other kinds of names. In JavaScript, for example, functions that construct objects are conventionally written in PascalCase, with the first letter capitalised, while ordinary variables and functions use camelCase. That visual distinction carries meaning: a capitalised name signals a constructor, a lowercase name signals an ordinary value. Breaking the convention does not break the program, but it removes a signal that readers rely on, and removing signals makes code harder to read.

CamelCase also interacts with the language's case sensitivity, which Section 42 covers. Because JavaScript treats totalPrice and totalprice as different variables, a name that is inconsistently cased across its uses will produce errors or, worse, two variables where one was intended. Writing names in camelCase from the start, and writing them the same way every time, prevents that class of mistake. It also helps with searching: a search for totalPrice finds every use, while a search for a name that appears in several casings finds only some of them. Consistency is therefore not just aesthetic; it is a practical aid to finding and changing code.

There are neighbouring conventions worth knowing so you can recognise them when you meet them. Snake_case, with underscores between words, is common in some other languages and appears in JavaScript code written by developers from those traditions, as well as in some constant names. Kebab-case, with hyphens, is used in HTML attributes and CSS class names but cannot be used in JavaScript variable names at all, because the hyphen is read as a minus sign. Uppercase with underscores is used for constants that are fixed for the whole program. None of these is wrong in itself, but in JavaScript, for ordinary variables, camelCase is the house style, and adopting the house style of the language you are writing in is a mark of professionalism. It says you are writing for readers, not just for the computer.

Check: Write "shopping cart total" as a camelCase variable name.

Section 12 — Assignment is not equality

One of the most common beginner mistakes in every programming language is confusing assignment with equality, because both are written with symbols that look like the equals sign from mathematics, and both involve the idea of two things being the same. In JavaScript, a single equals sign is assignment: it puts a value into a variable. The line let x = 5 does not ask whether x equals 5, and it does not state a mathematical truth about x. It performs an action: take the value 5 and store it in the variable called x, replacing whatever x held before. After the line runs, x holds 5, and the line itself is done. Assignment is a verb, not a question.

Equality, by contrast, is a question, and it is asked with different symbols: === for strict equality, as Section 19 explains, and == for loose equality, as Section 20 explains. The expression x === 5 does not change x; it produces a boolean, true if x currently holds 5 and false otherwise, and that boolean can be used in a condition or stored in another variable. The difference between = and === is therefore the difference between doing something and asking something, and mixing them up produces bugs that can be hard to spot precisely because the code looks almost right. A condition written as if (x = 5) does not test x; it assigns 5 to x and then tests the assigned value, which is truthy, so the condition always passes, and x has been silently changed in the process. That is a bug that produces no error message and can survive for a long time.

The confusion is understandable, because mathematics uses a single equals sign for equality and has no assignment at all. Programming needs both operations, so it needs two notations, and JavaScript's choice — = for assignment, === for equality — is the standard across most languages in its family. The way to keep them straight is to read them differently when you read code aloud. Read = as "gets" or "is assigned": "x gets 5". Read === as "is equal to": "x is equal to 5". That spoken distinction, practised while reading, builds a habit that survives into writing, where the distinction matters most.

There is a defensive habit that some developers use: writing comparisons with the literal value first, as in if (5 === x), so that an accidental single equals sign produces an error rather than a silent assignment, because you cannot assign to a literal. That style, sometimes called Yoda conditions, is a matter of taste, and many developers find it harder to read. A simpler defence is attention plus testing: read conditions carefully, and test them with values that should fail as well as values that should pass, so a condition that always passes reveals itself. Either way, the underlying lesson is the same: assignment changes the world, equality asks about it, and code that confuses the two will mislead you precisely because it looks so familiar.

Check: What does a single = do in JavaScript?

Section 13 — Reassigning a let variable

A let variable can be given a new value after its declaration, and that is the whole reason let exists: some values change over time, and the language provides a way to record those changes. The syntax for changing a value is plain assignment, without let: score = 10 changes the variable declared earlier as let score = 0, replacing its contents with 10. The absence of let on the second line is significant. Let means "create a variable"; assignment without let means "change the value of an existing variable". Using let a second time for the same name in the same scope is an error, because the language refuses to create two variables with the same name in the same place, as Section 7 explained. Reassign, do not redeclare.

let score = 0;
score = 10; // fine

Reassignment is how programs track change. A score starts at 0 and grows as points are earned. A total starts at 0 and accumulates as items are added. A current value starts as the first item and moves to the next as the user navigates. In each case, the variable is the same box with the same label, and its contents change to reflect the world. That continuity is important: code elsewhere that reads the variable always gets the current value, not the starting value, because it reads the box, not a snapshot. If programs could not reassign, every change would require a new variable, and tracking a score through a game would become a trail of score1, score2, score3, which is unworkable. Reassignment is what lets one name follow a changing reality.

There are habits that make reassignment safe. The first is to keep the number of reassignments small and visible: a variable reassigned in one place is easy to follow, while a variable reassigned in ten places is hard to reason about, because its value at any point depends on which of those places ran most recently. The second is to prefer const for values that do not need to change, so that the let variables in a program are exactly the moving parts, and a reader can see at a glance what moves and what stays. The third is to initialise sensibly, so that a variable's value before its first reassignment is meaningful rather than undefined, unless undefined is genuinely the right starting state.

A final point about reassignment and type: assigning a new value to a let variable does not require the new value to be the same type as the old one. A variable that held a number can later hold a string, and JavaScript will not complain, because variables are not typed — values are. That flexibility is part of the language's character, and it is occasionally useful, but as a habit it is better to keep a variable's type stable across its life. A variable called score that sometimes holds a number and sometimes holds a string is a variable whose readers cannot predict what they will find, and unpredictability is the seed of bugs. Let the variable change its value, but let it keep its kind.

Check: After let score = 0, how do you change score to 10?

Section 14 — Expressions

An expression is any piece of code that produces a value. That definition covers a surprising amount of territory: a literal like 42 is an expression, because it produces the value 42. A variable name like score is an expression, because it produces the value score currently holds. An arithmetic combination like 2 + 3 is an expression, because it produces 5. A comparison like age >= 18 is an expression, because it produces a boolean. A function call is an expression, because it produces whatever the function returns. Even a template literal with values embedded in it is an expression, because it produces a string. If you can ask "what value does this produce?" and get an answer, you are looking at an expression.

Expressions are distinguished from statements, which are the larger units that do things: a declaration creates a variable, an if statement chooses a branch, a loop repeats a block, a return statement exits a function. Statements are built from expressions, and most lines of code contain at least one expression, often several. The line let total = price + tax is a statement — a declaration — whose right-hand side is an expression, price + tax, which produces the value that gets stored. The line console.log(total) is a statement — a call — whose argument is an expression, total, which produces the value to be printed. Seeing the two levels, statements made of expressions, is a useful way to read code: the statement says what kind of action is happening, and the expressions say what values are involved.

Expressions combine into larger expressions, which is where their power lies. The expression price + tax can be part of (price + tax) * quantity, which can be part of a comparison like (price + tax) * quantity > budget, which can be part of a condition in an if statement. Each level produces a value that the next level uses, and the final value is what the statement acts on. That composability is why operators have precedence rules, as Sections 37 and 38 explain: when an expression contains several operators, the language needs rules for which combination happens first, so that every reader and every run agrees on the result. Brackets let you override those rules and, more importantly, let you show your intent.

Learning to see expressions is a reading skill that pays off immediately. When you meet a line you do not understand, ask what value each part produces, working from the inside out. The innermost values are literals and variables. The next level combines them with operators. The outermost level is the value the statement uses. That inside-out reading turns a complicated line into a series of small, understandable steps, and it is the same skill you will use when debugging: find the sub-expression whose value is wrong, and you have found the neighbourhood of the bug. Expressions are the atoms of programs, and fluency with them is fluency with the language itself.

Check: What value does the expression 2 + 3 produce?

Section 15 — Arithmetic operators

JavaScript's arithmetic operators are the symbols you know from school mathematics, with one addition. Plus (+) adds, minus (-) subtracts, asterisk (*) multiplies, and slash (/) divides. The newcomer is percent (%), the remainder operator, sometimes called modulo, which gives what is left over after division: 10 % 3 is 1, because 3 goes into 10 three times, using 9, with 1 left over. The remainder operator is less familiar but remarkably useful: it tests whether a number is even (a number is even when number % 2 is 0), it cycles through repeating patterns (count % 4 cycles through 0, 1, 2, 3), and it wraps values around a range, which is handy for carousels, calendars and games.

let total = 10 + 5; // 15
let left = 10 % 3;  // 1

Arithmetic in JavaScript follows the same rules as school arithmetic for ordinary cases: addition and subtraction are exact for whole numbers within the safe range, multiplication behaves as expected, and division produces decimals when the result is not whole, so 7 / 2 is 3.5 rather than 3. That last point differs from some languages, where dividing two whole numbers gives a whole number, and it is worth noting because it affects how you think about division: in JavaScript, division always produces the mathematically correct quotient, including fractions, unless the result is infinite or not a number, as Sections 26 and 27 explain.

Two cautions belong with arithmetic. The first is that the plus operator does double duty: with numbers it adds, but with strings it concatenates, as Section 16 explains, and when one side is a string and the other is a number, the number is converted to a string and concatenation wins. That means a calculation involving a value from a form field, which arrives as a string, can produce concatenation instead of addition unless the value is converted first. The second caution is decimal precision, covered in Section 31: decimal fractions are stored approximately in binary, so some decimal calculations produce results with tiny errors in their final digits. Neither caution should discourage you from arithmetic; both are reasons to check results with console.log when a calculation surprises you, and to convert explicitly when a value's type is uncertain.

Arithmetic operators combine into expressions of any complexity, and the precedence rules from Section 37 determine the order: multiplication, division and remainder before addition and subtraction, with brackets overriding everything. A formula like total = price * quantity + tax is read as price multiplied by quantity, plus tax, which matches the usual mathematical reading. When a formula's intended order differs from the default, brackets make the intention explicit, and explicitness is always worth the extra characters. Arithmetic is the oldest part of programming, and it remains the most used: totals, averages, positions, counts, sizes, scores — nearly every program spends much of its life adding, subtracting, multiplying and dividing.

Check: What is 10 % 3, and why?

Section 16 — Adding strings together

When the plus operator is used with strings, it joins them instead of adding: "Hello, " + "Ada" gives "Hello, Ada". This operation is called concatenation, from a Latin word meaning "chaining together", and it is how programs build messages out of pieces. A greeting is a fixed part ("Hello, ") plus a variable part (the user's name). An address line is a street, a town and a postcode joined with spaces and commas. A sentence reporting a result is a label plus a value plus a unit. Concatenation is the mechanism behind all of these, and it is used constantly in front-end work, because pages are largely text assembled from fixed words and variable data.

let name = "Ada";
let message = "Hello, " + name; // "Hello, Ada"

Concatenation follows a simple rule: if either side of a plus is a string, the other side is converted to a string and the two are joined. That rule is why "Total: " + 25 gives "Total: 25", with the number converted to text, and why "5" + 3 gives "53" rather than 8. The conversion is automatic, which is convenient when you want to display a number as part of a message, and treacherous when you wanted arithmetic but one value arrived as a string, as Section 24 explains in the context of coercion. The same operator doing two jobs — addition for numbers, joining for strings — is the single most common source of "why is my total a string?" bugs, and recognising the pattern is a debugging skill in itself: when a calculation produces a joined string instead of a sum, one of the values is a string, and converting it explicitly will fix the calculation.

Building longer messages by concatenation works, but it becomes awkward as the number of pieces grows. A sentence with three values joined by plus signs is readable; a paragraph with ten values is a thicket of quotes and plus signs that is easy to get wrong and hard to read. That is why template literals, covered in the next section, are the preferred tool for messages with several values: they let you write the message as a single piece of text with values dropped into place, rather than as a chain of fragments. Concatenation remains the right tool for simple joins — a first name plus a space plus a last name, a label plus a value — and for cases where you are building a string piece by piece in a loop or across several lines.

A few practical details round out the picture. Joining does not change the original strings, because strings are immutable, as Section 30 explains; it produces a new string, which you must store if you want to keep it. And joining an empty string to a value is a harmless way to convert that value to a string, though explicit conversion with String(), as Section 25 explains, says your intent more clearly. Concatenation is simple, but simple tools used carelessly still produce bugs, and the care here is about types and spaces.

Check: What does "Hello, " + "Ada" produce?

Section 17 — Template literals

Template literals are strings written with backticks instead of quotes, and they add two powers that ordinary strings lack. The first is interpolation: inside a template literal, a placeholder written as ${} is replaced by the value of the expression inside it. The second is multi-line text: a template literal can span several lines, and the line breaks become part of the string, without any special markers. Together, these powers make template literals the best tool for building messages that contain values, which is a large share of the text a front-end program produces.

let name = "Ada";
let message = `Hello, ${name}!`; // "Hello, Ada!"

Interpolation accepts any expression, not just variable names. Inside ${} you can write arithmetic, a function call, a comparison, or a conditional expression, and its result is converted to a string and placed into the message. That flexibility means a template literal can build a complete sentence in one readable line: You have ${items} items totalling ${total} naira. reads almost like the sentence it produces, with the variable parts visible in place. Compare that with the concatenation version, "You have " + items + " items totalling " + total + " naira.", which requires the reader to mentally reassemble the sentence from fragments, tracking spaces and punctuation across plus signs. The template version is easier to write correctly, easier to read, and easier to change when the sentence needs rewording, because the words stay in order.

Multi-line template literals are equally useful. A message that spans several lines — an email body, a multi-line address, a block of HTML being built by a script — can be written as it will appear, with line breaks in the code becoming line breaks in the string. Ordinary strings cannot do this without escape sequences for new lines, which make the code harder to read. There is one caution: indentation inside a multi-line template literal becomes part of the string, so code indented for readability can produce strings with unwanted leading spaces on each line. When exact formatting matters, that indentation must be accounted for, either by writing the literal without indentation or by trimming the result.

Template literals are now the default choice for any string that contains a value, and many codebases use them for all strings out of consistency, though ordinary quotes remain perfectly fine for fixed text. A note on the backtick character itself: it sits on most keyboards near the top-left, sharing a key with the tilde, and it is easy to confuse with a single quote at a glance. If a template literal fails with a syntax error, check that you used backticks rather than quotes, and that every ${ has a matching }. Those two small checks resolve most template literal problems, and the reward for the small adjustment in typing is messages that read like the sentences they produce.

Check: What does Hello, ${name}! produce when name is "Ada"?

Section 18 — Comparison operators

Comparisons are questions about order and size, and they produce booleans. The four ordering operators are greater than (>), less than (<), greater than or equal to (>=), and less than or equal to (<=). Each takes two values and answers whether the relationship holds: 5 > 3 is true, 5 < 3 is false, 5 >= 5 is true, 5 <= 4 is false. These operators are the vocabulary of thresholds and limits in programs: old enough to enter, cheap enough to buy, long enough to qualify, small enough to fit. A rule like "show this only if the user is 18 or older" becomes the comparison age >= 18, and the boolean it produces drives the decision that follows.

let adult = 20 >= 18; // true

Comparisons work most naturally on numbers, where order is unambiguous. They also work on strings, where order is determined by comparing characters one at a time using their underlying codes, which roughly matches alphabetical order for simple lowercase text but can surprise with capital letters, accented characters, and mixed case, because capital letters sort before lowercase ones in that underlying order. For most front-end purposes, comparing strings for order is less common than comparing them for equality, and when string ordering does matter — sorting a list of names, for example — dedicated sorting tools handle the subtleties better than raw operators. The practical rule is to use ordering operators confidently on numbers and cautiously on strings, testing with representative examples when strings are involved.

Comparisons combine with logic, which Section 22 covers, to express compound rules. "Old enough and has a ticket" is age >= 18 && hasTicket. "Either a member or a guest with a pass" is isMember || hasGuestPass. "Within the range" is value >= minimum && value <= maximum, a pattern so common it deserves memorising: a range check is two comparisons joined by AND. These compound conditions are where comparisons earn their keep, because real-world rules are rarely single thresholds. A discount might require a total above a minimum and a membership flag; a form might require a value that is not empty and not longer than a limit; a game might require a score within a band. Each of those is comparisons plus logic, and the resulting condition, stored in a well-named boolean, reads like the rule it implements.

One caution about comparisons and equality: the ordering operators do not convert types as aggressively as the loose equality operator does, but mixing types in a comparison can still produce surprising results, because JavaScript will convert where it can. Comparing a number with a string that looks like a number converts the string, so 5 > "3" is true, which is reasonable, but comparing values of wildly different types can produce results that are hard to predict. The defensive habit is the same as elsewhere in this chapter: know your types, convert explicitly when a value's type is uncertain, and test comparisons with the kinds of values they will actually receive, including edge cases like equal values, where the difference between > and >= decides the outcome.

Check: What value does 5 > 3 produce?

Section 19 — Strict equality with ===

Equality in JavaScript comes in two strengths, and the strong one is the one you should use by default. Strict equality, written ===, checks whether two values are equal in both value and type, with no conversion of any kind. The number 5 is strictly equal to the number 5, because both the value and the type match. The number 5 is not strictly equal to the string "5", because although the characters match, the types do not. The boolean true is not strictly equal to the number 1, even though they are related in some conversion tables, because their types differ. Strict equality does exactly what it looks like it does, and that predictability is its virtue.

5 === 5;   // true
5 === "5"; // false

The importance of strict equality becomes clear when you consider what the alternative does. Loose equality, covered in the next section, converts values before comparing them, and those conversions follow rules that are long, surprising, and difficult to memorise completely. A developer using loose equality must hold those rules in mind for every comparison, and must trust that every future reader will hold them too. A developer using strict equality needs only one rule: same value, same type, or it is not equal. That simplicity reduces bugs, because a comparison that looks equal but involves different types is usually a sign that one value is not what you thought it was — a string where a number was expected, for example — and strict equality surfaces that discrepancy instead of hiding it behind a conversion.

Strict equality has two famous edge cases worth knowing. The first is NaN: NaN === NaN is false, because NaN is defined as not equal to anything, including itself, which is why Section 26 recommends Number.isNaN for testing it. The second is objects and arrays: strict equality for objects checks identity, not contents, so two separately created objects with identical properties are not strictly equal, because they are different objects in memory. That behaviour belongs more properly to the later chapters on objects, but it is worth noting here so that an early experiment with object equality does not confuse you. For numbers, strings, booleans, null and undefined — the values this chapter focuses on — strict equality behaves exactly as described, with NaN as the sole exception.

The habit to form is absolute: use === unless you have a specific, articulable reason to use something else, and use !==, its strict "not equal" partner, in the same way. Linters and style guides in professional codebases commonly enforce this rule automatically, which tells you how widely it is accepted. When you encounter loose equality in old code, read it as a conversion happening implicitly, and consider whether the conversion is intentional or accidental. When you write new code, strict equality is the default, and your comparisons will mean what they appear to mean.

Check: Why is 5 === "5" false?

Section 20 — Loose equality with ==

Loose equality, written ==, also compares two values, but before comparing it converts them to a common type, following a set of rules that are long and, in places, surprising. The headline example is 5 == "5", which is true, because the string "5" is converted to the number 5 before the comparison. That particular conversion feels reasonable, and it is the reason loose equality exists: in the early web, values from forms and addresses arrived as strings, and comparing them with numbers without explicit conversion was convenient. But the conversion rules go well beyond that friendly case, and their full extent is a frequent source of bugs and interview questions alike.

The rules, in brief, are these. If the types are the same, loose equality behaves like strict equality. If one side is null and the other is undefined, they are considered equal to each other and to nothing else. If one side is a number and the other is a string, the string is converted to a number. If one side is a boolean, it is converted to a number, with true becoming 1 and false becoming 0, before any further conversion. If one side is an object, it is converted to a primitive value first, which can produce results that are difficult to predict without knowing the object's conversion behaviour. The combination of these rules produces results like "" == 0 being true, " " == 0 being true, and false == "0" being true, none of which matches most people's intuition about equality, and all of which have caused real bugs.

That unpredictability is why most developers avoid loose equality and use strict equality instead, as the previous section recommends. The argument is not that loose equality is broken; it is that its behaviour requires memorising a table of conversions, and code that requires a table to understand is code that will be misunderstood. Strict equality requires one rule. When a conversion is genuinely needed — when a form value that arrived as a string must be compared with a number — the clearer approach is to convert explicitly first, with Number(), and then compare strictly, so the conversion is visible in the code rather than hidden inside the operator. Visible conversions can be reviewed, questioned and tested; hidden conversions can only be memorised.

You will still meet loose equality in old code, in answers online, and in code written by developers who learned JavaScript before strict equality became the consensus. When you meet it, read it carefully: ask what conversions it performs on the actual values involved, and whether those conversions are what the author intended. If you are maintaining such code, consider replacing loose comparisons with explicit conversion plus strict comparison, as a safe improvement that makes intent clearer without changing behaviour in the cases that matter. And in your own code, let loose equality remain a piece of knowledge you understand but do not use, like a shortcut you know exists but have chosen not to take because the main road is safer.

Check: Why do most developers prefer === over ==?

Section 21 — Not equal

Every equality operator has a "not equal" partner, and strict equality's partner is !==. It is true when two values differ in value or in type, and false when they are strictly equal. The number 5 is not strictly unequal to itself, so 5 !== 5 is false. The number 5 is strictly unequal to the string "5", because their types differ, so 5 !== "5" is true. The logic mirrors strict equality exactly, with the answer inverted, which makes !== as predictable as === and equally safe to use by default. When you want to say "these two things are different", !== says it plainly.

5 !== 5;   // false
5 !== "5"; // true

The loose partner, !=, mirrors loose equality in the same way: it converts types before comparing, and then inverts the result. The value 5 != "5" is false, because loose equality considers them equal, and not-equal inverts that to false. The same objections that apply to loose equality apply to loose inequality, for the same reason: hidden conversions make the result harder to predict than the code suggests. A comparison like value != "" might seem to test whether a value is empty, but loose conversion means it also treats 0 and false as equal to the empty string, which may or may not be what the author intended. When intent matters — and in a comparison, intent is everything — explicit conversion plus strict inequality is clearer: convert the value first, then test with !==, so the reader can see exactly what is being compared.

Not-equal operators appear most often in guard clauses and conditions that exclude a case. "If the status is not 'complete', show the pending message" is status !== "complete". "If the user is not logged in, redirect to the login page" is a condition built on a boolean being false, which can be written as loggedIn === false or as !loggedIn, the NOT operator from the next section. Choosing between !== and ! is a matter of what reads most clearly in context: !== is natural when comparing against a specific value, while ! is natural when flipping a boolean. Both are common, and both are preferable to the convoluted alternative of testing equality and then negating the whole condition in a way that obscures the intent.

There is a small readability point worth forming as a habit. Conditions written as "not equal" are slightly harder to follow than conditions written positively, because the reader must hold a negation in mind while reading the rest of the branch. When a choice between a positive and a negative condition is available — testing whether a value is present rather than whether it is not absent, for example — the positive form is usually clearer. That is a guideline, not a rule: some conditions are naturally negative, and forcing them positive produces awkward code. But when you find yourself writing a condition with two negations, or a not-equal test inside an already negative branch, pause and see whether the logic can be restated positively. Clarity in conditions is clarity in the program's decisions, and decisions are the part of a program most worth making readable.

Check: What does 5 !== 5 evaluate to?

Section 22 — Logical operators

Logical operators combine booleans into new booleans, and they are the grammar of compound conditions. AND, written &&, is true only when both sides are true: true && true is true, while any combination involving false is false. OR, written ||, is true when at least one side is true: true || false is true, false || true is true, and only false || false is false. NOT, written !, flips a boolean: !true is false, !false is true. Together, these three operators express every compound rule a program needs: both of these, either of these, not this. Real-world rules are almost always compound — "logged in and has items", "a member or a guest with a pass", "not finished yet" — and logical operators are how those rules are written.

let canBuy = true && true;  // true
let canEnter = true || false; // true
let locked = !true;          // false

Logical operators have two behaviours beyond their truth tables worth knowing. The first is short-circuiting. For AND, if the left side is false, the result must be false regardless of the right side, so JavaScript does not evaluate the right side at all. For OR, if the left side is true, the result must be true regardless of the right side, so JavaScript skips the right side. That skipping matters when the right side has a side effect, or when evaluating it would cause an error, like reading a property of a missing value. A pattern like user && user.name is safe precisely because of short-circuiting: if user is missing, the right side never runs, and no error occurs. Short-circuiting also means logical operators return one of their operands, not necessarily a boolean — a detail that later chapters use deliberately, but that beginners should simply be aware of when a logical expression produces a value that is not true or false.

The second behaviour is precedence. NOT applies before AND, and AND applies before OR, which matches the conventions of ordinary logic and mathematics. An expression like a || b && c is read as a || (b && c), not as (a || b) && c. When a compound condition's intended grouping differs from the default, or when the default is not obvious to a reader, brackets make the grouping explicit, and explicitness is worth the characters. A condition like (isMember || hasGuestPass) && eventOpen says plainly that either kind of entry, combined with the event being open, grants admission, while the unbracketed version requires the reader to recall the precedence rules to be sure. Brackets are free, and they remove doubt.

Building compound conditions well is a skill of naming as much as of logic. A long condition written inline in an if statement is hard to read and hard to test, while the same condition stored in a well-named boolean — canEnter, isEligible, shouldShow — is self-explanatory. When a condition grows beyond two or three parts, consider storing its parts in named booleans first, then combining those, so each part can be understood and tested separately. Logic is the part of programming where a small investment in clarity pays the largest dividend, because conditions are where programs decide, and decisions are where bugs do the most damage.

Check: What does true && false evaluate to?

Section 23 — Truthy and falsy

JavaScript often needs a boolean in a place where a value of another type appears — most commonly as the condition of an if statement — and in those places it converts the value to a boolean automatically. Most values convert to true and are called truthy. A short, specific list converts to false and is called falsy: the boolean false itself, the number 0, the empty string "", null, undefined, and NaN. Everything else — every non-empty string, every non-zero number, every object, every array — is truthy. That list is short enough to memorise, and memorising it is worthwhile, because it explains a great deal of everyday JavaScript behaviour.

The most useful consequence is that testing a value's presence can be written simply. The condition if (name) is true when name holds a non-empty string and false when name holds an empty string, null, or undefined. That single condition therefore covers "was a name provided?" across several kinds of absence, without needing to list them. Similarly, if (items.length) is true when a list has items and false when it is empty, because the length is a non-zero number in the first case and 0, a falsy value, in the second. These idioms are everywhere in real code, and they read naturally once the falsy list is familiar: the condition asks "is there something here?" and the language answers based on the value's truthiness.

There are traps in the same territory. The string "0" is truthy, because it is a non-empty string, even though the number 0 is falsy, which surprises developers who expect the two to behave alike. The string "false" is truthy, because it is a non-empty string, even though it reads like the boolean false, which is a common source of confusion when values arrive as text from a form or an address. An empty array is truthy, even though it contains nothing, because all objects are truthy, which surprises developers who expect an empty collection to be falsy. Each of these traps is resolved by the same remedy: when the distinction matters, test explicitly rather than relying on truthiness. Test a string's length when you care about emptiness, test an array's length when you care about whether it has items, and test a value against null or undefined when you care about presence specifically.

Truthiness is a convenience, not a substitute for precision. Use it for the idioms where it is clear — if (name) for "a name was provided", if (error) for "an error occurred" — and use explicit comparisons where the exact condition matters. That balance keeps code both concise and correct, and it is a balance that experienced developers strike instinctively after meeting the traps once or twice. Meet them here, in explanation, and you may skip the debugging sessions they would otherwise have cost you.

Check: Is an empty string truthy or falsy?

Section 24 — Type coercion

Coercion is JavaScript quietly converting a value from one type to another, usually when an operator meets values of mixed types. It is not an error and it is not a bug in the language; it is a design decision, made in JavaScript's earliest days, to be forgiving about types rather than strict. When that forgiveness matches your intention, it feels convenient. When it does not, it produces the bewildering results that fill this section and several others: a calculation that concatenates instead of adding, a comparison that returns true for values that look different, a condition that passes for a value you considered empty. Understanding coercion is understanding why those results happen, and why explicit conversion is usually better.

The most frequent coercion involves the plus operator. When plus meets two numbers, it adds. When it meets a string on either side, it converts the other side to a string and concatenates. That is why "5" + 3 gives "53": the number 3 is converted to the string "3", and the two strings are joined. The same rule explains why a total built from form values goes wrong: form values arrive as strings, so price + tax with string values concatenates instead of adding, and a total that should be 15 becomes "105". The fix is conversion before calculation, as Section 25 explains, but the diagnosis is coercion: the operator did not change its meaning, the types changed what the operator did.

Other operators coerce differently, which is part of what makes coercion confusing. Subtraction, multiplication and division convert strings to numbers rather than converting numbers to strings, so "5" - 3 gives 2, not "53" or an error. Comparisons convert in their own ways, as Sections 19 and 20 explain. Conditions convert to booleans according to the truthy and falsy rules of Section 23. Each operator has its own conversion behaviour, and the full set of rules is long. No developer memorises all of it, and no developer needs to, because the practical response is uniform: when a value's type is uncertain, convert it explicitly before using it, and the operator's hidden conversion never gets a chance to surprise you.

Coercion is sometimes described as a flaw in JavaScript, and sometimes defended as flexibility, and the truth is that it is both, depending on context. In quick scripts and in code that handles user input loosely, coercion saves keystrokes. In larger programs, where values flow through many functions and their types are harder to track, implicit conversion becomes a source of bugs that are hard to trace, because the conversion happens silently at the point of use rather than visibly at the point of entry. The professional consensus, reflected throughout this book, is to prefer explicit conversion: say what you mean, in code, at the place where a value enters a calculation or a comparison, and let the operators work on values whose types you have already settled.

Check: What does "5" + 3 produce, and why?

Section 25 — Converting types on purpose

Instead of relying on JavaScript's quiet coercion, you can convert a value's type explicitly, saying in code exactly what you mean. The three conversion functions are Number(), String() and Boolean(), each taking a value and returning a value of the named type. Number("42") gives the number 42. String(42) gives the string "42". Boolean(1) gives true, following the truthy and falsy rules. These functions are the visible, reviewable alternative to hidden conversion, and using them is one of the clearest marks of careful code.

Number("42"); // 42
String(42);   // "42"

Each conversion has its own behaviour with edge cases, and knowing them prevents surprises. Number() converts a string that looks like a number into that number, converts an empty string to 0, and converts a string that does not look like a number into NaN, which Section 26 explains. It converts true to 1 and false to 0, and converts null to 0 and undefined to NaN, which are results worth knowing but rarely relied upon deliberately. String() converts almost anything into a readable string: numbers become their digits, booleans become "true" or "false", null becomes "null", and undefined becomes "undefined". Boolean() follows the truthy and falsy list exactly: falsy values become false, everything else becomes true. None of these conversions changes the original value; each returns a new value of the new type, which you must store or use if you want to keep it.

Explicit conversion shines at the boundaries of a program, where values enter from outside. Form fields give strings; convert them with Number() before calculating. A value to be displayed gives a number; convert it with String() or, more commonly, embed it in a template literal, which converts implicitly but visibly in context. A setting that arrives as text — "yes", "true", "1" — needs a deliberate conversion to boolean, because Boolean("false") is true, since any non-empty string is truthy, and that trap catches developers who assume the function parses the word. For text-to-boolean conversion, compare the string explicitly, as in value === "true", rather than relying on Boolean(). That example illustrates the deeper principle: explicit conversion is not just about calling a function; it is about stating the rule by which a value changes type, in a way a reader can verify.

The habit to form is to convert at entry, once, rather than at every use. When a value arrives from a form, convert it immediately and store the converted value, so the rest of the code works with a number and never needs to wonder. When a value is about to be displayed, convert it at the point of display, so the stored value remains in its natural type. Converting at boundaries keeps the interior of a program clean, with each variable holding a value of a known, stable type, and that stability is what makes the rest of the code predictable. Coercion happens everywhere; explicit conversion happens where you choose, and choosing is the difference.

Check: How do you convert the string "42" to the number 42 on purpose?

Section 26 — NaN, not a number

NaN stands for "Not a Number", and it is the value JavaScript produces when a calculation fails to yield a real number. Converting a string that does not look like a number with Number("hello") gives NaN. Subtracting a number from a string that cannot be converted gives NaN. Dividing zero by zero gives NaN. In each case, the operation was attempted on values that do not support it numerically, and rather than crashing, JavaScript returns NaN as a placeholder for "no numeric result". That placeholder then propagates: any arithmetic involving NaN gives NaN, so a single failed conversion early in a calculation can turn an entire total into NaN, which is often how the problem is first noticed — a price, a score or a total displaying as NaN on the page.

Number("hello"); // NaN
Number.isNaN(NaN); // true

NaN has two properties that confuse everyone who meets it. The first is that typeof NaN is "number". That sounds contradictory — how can Not a Number be of type number? — but it makes sense once you understand that NaN is a special value within the number type, a member of the type that represents the absence of a numeric result, much as null is a value that represents the absence of an object. The type system needs a way to say "this was supposed to be a number but is not", and NaN is that way. The second confusing property is that NaN is not equal to itself: NaN === NaN is false, and NaN == NaN is also false. That follows from the definition of NaN as an unknown or unrepresentable result: two unknown results cannot be assumed equal, because they might be different unknowns. It is logically consistent, but it breaks the usual expectation that a value equals itself, and it means you cannot test for NaN with equality.

The correct test is Number.isNaN(value), which returns true only when the value is exactly NaN, and false for everything else, including strings that would convert to NaN. There is also an older global function, isNaN(), which first converts its argument to a number and then tests the result, so isNaN("hello") is true even though the string "hello" is not itself NaN. That conversion makes the older function misleading, and Number.isNaN is the recommended test in modern code. When a calculation produces NaN, the debugging move is to work backwards: find the first operation in the chain that produced NaN, check the types of its inputs, and you will usually find a string that was not converted, a missing value, or a field that was empty. NaN is rarely the bug itself; it is the visible symptom of a type or data problem earlier in the chain, and treating it as a symptom rather than as the disease leads you to the fix faster.

Check: How do you test whether a value is NaN?

Section 27 — Infinity

Dividing by zero in JavaScript does not crash the program, unlike in some other languages where it raises an error. Instead, 1 / 0 gives Infinity, a special value of type number that represents a quantity larger than any finite number, and -1 / 0 gives -Infinity, its negative counterpart. Infinity is a real value that can be stored, compared and used in further calculations: Infinity + 1 is still Infinity, Infinity - Infinity is NaN, and any finite number is less than Infinity. It behaves, in other words, like the mathematical idea of infinity made concrete enough to compute with, and it exists so that division by zero produces a value rather than a crash.

Infinity usually appears as a symptom, not as an intention. A calculation that divides by a variable will produce Infinity when that variable turns out to be zero, which typically means the variable was not what the programmer expected — a count that was never incremented, a length that was empty, a value that was missing and defaulted to zero. Seeing Infinity in a result is therefore a diagnostic clue in the same way NaN is: something earlier in the calculation was zero when it should not have been, or a quantity grew without bound when it should have been limited. The debugging move is the same: work backwards to the division, check the divisor, and find out why it was zero. Fixing the divisor, or guarding against zero before dividing, resolves the symptom at its cause.

There are legitimate uses of Infinity, and knowing them prevents you from treating every Infinity as a bug. It is commonly used as an initial value when searching for a minimum: start with Infinity, and any real value encountered will be smaller, so the first comparison always replaces it. It is used as a bound in comparisons: a condition like value < Infinity is true for every finite number, which is occasionally useful as a default limit. And it appears in the language's own constants: Number.POSITIVE_INFINITY is Infinity, and Number.MAX_VALUE is the largest finite number, beyond which calculations overflow to Infinity. These uses are deliberate and documented, and they are distinct from accidental Infinity, which appears unexpectedly in a result that should have been finite.

The practical guidance is therefore twofold. First, when Infinity appears unexpectedly, treat it as evidence about a divisor, not as a value to display: guard divisions by checking for zero, or validate inputs before calculating, so that a user sees a meaningful message rather than the word Infinity on a page. Second, when you use Infinity deliberately, comment the intention, because a future reader who meets Infinity in code will otherwise assume it is a bug. Infinity is a reminder that JavaScript prefers to produce a value rather than to crash, and that preference places a responsibility on the programmer: to check results that could be infinite or not-a-number, and to translate them into something meaningful before they reach a human being.

Check: What does 1 / 0 evaluate to in JavaScript?

Section 28 — String length and positions

Every string has a length property that tells you how many characters it contains, and every character in a string has a position, called an index, starting from 0. The string "hello" has length 5, its first character is at position 0 and holds "h", its second character is at position 1 and holds "e", and its last character is at position 4 and holds "o". There is no character at position 5; asking for "hello"[5] gives undefined, because the positions run from 0 to length minus 1. That zero-based counting is one of the first conventions a new programmer must internalise, because it differs from everyday counting, where the first item is item one, and it is the source of a whole family of off-by-one mistakes.

let word = "hello";
word.length; // 5
word[0];     // "h"

Length is a property, not a method, which means you read it without brackets: word.length, not word.length(). That distinction — properties are read, methods are called — runs throughout JavaScript, and strings are a good place to learn it, because they have both: length is a property, while toUpperCase() and the other methods of the next section are called with brackets. The length property is also the basis for many common checks: whether a string is empty (length === 0), whether it exceeds a limit (length > maximum), and where its last character sits (length - 1). Those checks appear constantly in form validation, in display logic, and in any code that processes text, so fluency with length is fluency with a daily tool.

Indexing with square brackets gives you a single character, and it is the simplest way to examine a string's contents. It is also the basis for understanding loops over strings, which a later chapter covers: visiting each character in turn means visiting positions 0 through length minus 1, and stopping at length minus 1 rather than at length is the detail that prevents reading past the end. A related caution is that characters outside the basic range — emoji and some characters from other writing systems — can occupy two positions in JavaScript's internal counting, because the language counts units of its internal encoding rather than visible characters. For ordinary text in English and most European languages, length matches the visible character count, but when emoji are involved, length can exceed the number of visible symbols, which is a known subtlety to be aware of rather than a problem to solve today.

The habit to form is to think of a string as a row of boxes, numbered from zero, with length telling you how many boxes there are. When you need the first character, take position 0. When you need the last, take position length minus 1. When you need to know whether a string is empty, check its length. That mental image, applied consistently, prevents the most common string errors, and it transfers directly to arrays, which use the same zero-based indexing and the same length property. Strings and arrays share this shape, and learning it once serves you twice.

Check: What is "hello"[1]?

Section 29 — Useful string methods

Strings come with built-in methods: functions attached to the string that perform common operations and return a result. The most useful for beginners are a small set. ToUpperCase() returns the string with all letters in capitals; toLowerCase() returns it in lowercase. Trim() returns the string with spaces removed from both ends, which is invaluable for cleaning up user input, where a name typed with accidental spaces should not be treated as different from the same name without them. Includes() returns a boolean saying whether one string contains another, which is how you test whether a sentence mentions a word or whether an address contains a particular town. Each of these is called with a dot and brackets, and each returns a new string or a boolean, leaving the original string unchanged, because strings are immutable, as the next section explains.

"  Hello ".trim();        // "Hello"
"Hello".toLowerCase();    // "hello"
"Hello".includes("ell");  // true

Case methods are useful for comparisons that should ignore case. A user who types "ADA" and a stored name "Ada" should probably be considered the same, and comparing their lowercase versions achieves that: "ADA".toLowerCase() === "ada".toLowerCase() is true, while the direct comparison is false. That pattern — normalise both sides to the same case before comparing — is a standard technique for usernames, email addresses, search terms, and any text where capitalisation should not matter. It is also a reminder that equality for strings is exact by default: "Ada" and "ada" are different strings, and only deliberate normalisation makes them equal.

Other methods extend the toolkit in ways you will meet as needed. Slice() extracts a portion of a string by position, which is how you take the first few characters of a longer text. Replace() returns a string with a portion swapped for something else. Split() breaks a string into an array at a separator, which is how a comma-separated list becomes individual items. IndexOf() returns the position where a substring first appears, or -1 if it does not appear, which is an older way of testing containment that includes() now handles more readably. You do not need to memorise all of them now; you need to know that string methods exist, that they are called on the string with a dot, and that they return new values rather than changing the original. When you need an operation, search for the string method that does it before writing your own loop, because the built-in method is tested, readable, and almost certainly what other developers would expect to see.

A final habit: when you chain methods, read the chain left to right as a series of transformations. The expression " Hello ".trim().toLowerCase() first trims the spaces, giving "Hello", then lowercases, giving "hello". Each step produces a new string that the next step transforms, and the final result is what the expression yields. Chaining reads like a pipeline, and it is a style you will see throughout JavaScript, not just with strings. Learning to read chains now makes later code, with longer chains over more complex data, feel familiar rather than intimidating.

Check: What does "Hello".toLowerCase() return?

Section 30 — Strings cannot be changed in place

Strings are immutable, which means that once a string exists, its characters cannot be changed. You cannot alter one character of a string and leave the rest as it was. Writing "hello"[0] = "H" does not change the string to "Hello"; in strict mode it throws an error, and outside strict mode it silently does nothing, leaving the string exactly as it was. That behaviour surprises developers who are used to changing items in a list, where assignment to a position works, but strings follow a different rule, and understanding it prevents a class of bugs where a change appears to have been made but has not taken effect.

let word = "hello";
word = "H" + word.slice(1); // "Hello"

Immutability does not mean strings are useless for transformation; it means transformation produces a new string rather than altering the old one. Every string method works this way: toUpperCase() returns a new string in capitals, leaving the original untouched; trim() returns a new string without surrounding spaces; replace() returns a new string with the replacement made. The pattern is always the same: take the old string, produce a new string, and if you want to keep the result, store it, usually by assigning it back to the same variable, as in the example, where word is reassigned to a new string built from "H" plus the remainder of the old word. Forgetting to store the result is the classic immutability bug: calling word.toUpperCase() without assigning it, and then wondering why word is still lowercase. The method worked; its result was simply discarded.

There are reasons languages make strings immutable, and they are worth knowing even at this stage. Immutability makes strings safe to share: if two variables hold the same string, neither can change it underneath the other, so there are no surprises from shared state. It makes strings safe to use as keys and in comparisons, because a string's value cannot drift after it has been checked. And it allows the language to optimise storage, reusing the same underlying text for equal strings. These are design benefits that matter more as programs grow, but even in small programs, immutability has a practical benefit: it forces a style where transformations are explicit. Each step produces a visible new value, which can be logged, checked and stored, rather than silently mutating a value that other code may be relying on.

The working rule is simple: when you want a changed string, build a new one and store it. Use methods that return new strings, use concatenation or template literals to assemble new strings from pieces, and assign the result to a variable. When a string operation appears to do nothing, check whether you stored its result. That single check resolves most string transformation bugs, and it is a habit that transfers to other immutable values and to the functional style of programming that later chapters introduce, where producing new values rather than mutating old ones is a deliberate and valued approach.

Check: Why does "hello"[0] = "H" not change the string?

Section 31 — Decimal numbers are approximate

Computers store numbers in binary, using a fixed number of bits, and some decimal fractions cannot be represented exactly in binary, just as the fraction one third cannot be represented exactly in decimal — it becomes 0.333... going on forever. The decimal 0.1 is one of those fractions: in binary it is a repeating pattern that must be cut off at the limit of the available bits, so the stored value is very slightly different from 0.1. When you add 0.1 and 0.2, those tiny differences accumulate, and the result is 0.30000000000000004 rather than the 0.3 that school arithmetic predicts. The computer is not malfunctioning; it is showing you the exact result of adding two approximations, and the approximation is inherent to the representation, not a bug in JavaScript. Every language that uses the same standard binary floating-point representation has the same behaviour.

0.1 + 0.2; // 0.30000000000000004

For most purposes, this imprecision is harmless. A measurement, a position, a size, or a score that is off by a trillionth does not matter, because no human can perceive the difference and no decision depends on it. The place where it matters is money, because money is counted in exact minor units and because humans notice when a total is a fraction of a kobo or cent wrong, and because financial calculations must reconcile exactly. The standard solution is to avoid decimal fractions for money altogether: store amounts as whole numbers of the smallest unit — kobo instead of naira, cents instead of dollars — so that all arithmetic is on integers, which are exact within the safe range. A price of 25.50 naira becomes 2550 kobo; adding, subtracting and multiplying kobo values gives exact results; and conversion to naira happens only when displaying the amount, by dividing by 100 and formatting to two decimal places. That single practice eliminates the entire class of money-rounding bugs.

When display rounding is needed for non-money decimals, JavaScript provides tools. The method toFixed() returns a string of a number rounded to a given number of decimal places: (0.1 + 0.2).toFixed(2) gives "0.30". Note that it returns a string, not a number, because its purpose is display, and if you need to calculate further, you should convert back or, better, keep the unrounded number for calculation and round only for display. Rounding for display and rounding for calculation are different operations with different purposes, and confusing them — rounding intermediate results during a calculation — introduces errors of its own. The rule is to calculate with full precision and round once, at the moment a human reads the result. For display purposes, a rounded string is usually enough, and most pages never need more precision than two decimal places for a human reader.

Check: Why does 0.1 + 0.2 not equal exactly 0.3?

Section 32 — The Math object

Math is a built-in object that holds mathematical helpers, available in every JavaScript program without any setup. You do not create it, you do not import it, and you do not call it as a function; you use its methods and properties directly, as in Math.round() or Math.PI. That design — a single shared object holding related utilities — is a pattern you will meet repeatedly in JavaScript, and Math is a gentle first example of it. The helpers it provides cover the operations that come up constantly: rounding, random numbers, maximums and minimums, powers and roots, and trigonometric functions for the rarer cases that need them.

Math.round(4.6); // 5
Math.floor(4.9); // 4

The rounding methods deserve careful distinction, because they are often confused. Math.round() rounds to the nearest whole number, with halves rounding up: 4.5 becomes 5, 4.4 becomes 4. Math.floor() always rounds down, towards negative infinity: 4.9 becomes 4, and -4.1 becomes -5, which surprises developers who expect "down" to mean "towards zero". Math.ceil() always rounds up, towards positive infinity: 4.1 becomes 5, and -4.9 becomes -4. Math.trunc() removes the fractional part entirely, rounding towards zero: 4.9 becomes 4, and -4.9 becomes -4. Choosing between them depends on the problem: rounding a display value usually means round(); fitting items into containers that must not overflow usually means ceil(), because you need enough containers for the remainder; counting complete items usually means floor(), because a partial item is not an item. Naming the need before choosing the method prevents the common error of using round() everywhere and wondering why a count is occasionally one too few.

Math.random() is the method beginners meet most fondly, because it generates a random decimal from 0 up to, but not including, 1. Scaling and flooring that result produces random integers in a range, a pattern worth memorising: Math.floor(Math.random() * 6) + 1 gives a random whole number from 1 to 6, like a die roll, because random() * 6 gives a value from 0 up to 6, flooring gives 0 to 5, and adding 1 shifts the range to 1 to 6. That pattern generalises: to get a random integer from 0 up to n minus 1, floor random() * n; to get one from a minimum to a maximum inclusive, floor random() * (maximum - minimum + 1) + minimum. Randomness in Math.random() is not suitable for security purposes, like generating passwords or tokens, because it is predictable in principle, but for games, shuffling, sampling and visual variety it is exactly right.

Other Math methods round out the toolkit. Math.max() and Math.min() return the largest and smallest of their arguments, which is useful for clamping a value into a range: Math.max(0, Math.min(100, value)) forces a value into the range 0 to 100. Math.pow() raises a number to a power, and Math.sqrt() gives a square root. Math.PI holds the constant pi, for calculations involving circles. You do not need to memorise the full list; you need to know that Math exists, that its methods are called with a dot, and that when a mathematical operation is needed, Math probably provides it.

Check: What does Math.floor(4.9) return?

Section 33 — Comments

A comment is a note for humans that JavaScript ignores completely. The language reads your code, executes the instructions, and skips comments as if they were not there, which means a comment can never change what a program does. That freedom is what makes comments useful: they are the one place in a file where you can speak directly to a human reader without worrying about syntax, and they are the tool for recording the things code cannot say for itself. Two forms exist. A line comment starts with // and comments out everything from those characters to the end of the line. A block comment starts with /* and ends with */, and can span several lines, which is useful for longer explanations or for temporarily disabling a block of code during debugging.

// Prices are stored in kobo to avoid decimal problems
let price = 2500;

The question is not how to write comments, but what to write in them, and the answer is: why, not what. Code already says what it does, to anyone who can read the language, so a comment that restates the code — "increment the counter" above count++ — adds nothing and becomes noise that readers learn to ignore. A comment that explains why — "prices are stored in kobo to avoid decimal problems", above a line that would otherwise look like an arbitrary choice — adds information that the code cannot carry, because the reason for a decision lives in the world, not in the syntax. Other good reasons for comments include warning about a trap ("do not change this order; the second call depends on the first"), explaining a workaround ("this check is needed because older browsers return null here"), and recording a decision that future readers might otherwise reverse ("kept as a string because the API expects text, not a number").

Comments have costs that are worth respecting. A comment that is wrong is worse than no comment, because it actively misleads, and comments become wrong when code changes and the comment is not updated with it. That is why the advice to comment "why" rather than "what" matters: "what" changes every time the code changes, while "why" often stays true across many versions of the code. It is also why short, clear code with good variable names needs fewer comments than convoluted code: when the code itself is readable, only the genuinely non-obvious decisions need explaining. A file drowning in comments is often a file whose code needs simplifying, not more annotation.

There is one more use of comments that belongs in a beginner's toolkit: temporarily disabling code. Commenting out a block during debugging, to test whether it is involved in a bug, is a legitimate technique, and it is the manual version of the divide-and-conquer method from the debugging chapter. The discipline is to remove such temporary comments when the debugging is done, or to convert them into a proper explanation if the disabled code revealed something worth recording. A codebase full of commented-out code from old debugging sessions is confusing, because readers cannot tell whether the code is disabled temporarily, preserved for reference, or simply forgotten. Comment with purpose, keep comments true, and let the code speak for itself wherever it can.

Check: What is a comment for, and what does JavaScript do with it?

Section 34 — Escaping characters in strings

Sometimes a string needs to contain a character that would otherwise end it or change its meaning. A string written in double quotes cannot simply contain a double quote, because the first inner quote would be read as the end of the string, and the rest would become a syntax error. The solution is escaping: placing a backslash before the character, which tells JavaScript to treat it as part of the string rather than as a marker. The sequence \" inside a double-quoted string means "a double quote character, not the end of the string", and the string continues after it. The same technique works for single quotes inside single-quoted strings, and for backticks inside template literals, and it is one of those small mechanisms that removes a whole class of frustration once it is understood.

let quote = "She said \"hi\"";

Escaping also provides characters that cannot easily be typed or seen. The sequence \n means a new line, so a string containing \n will break onto a new line when displayed. The sequence \t means a tab, \r means a carriage return, and \\ means a literal backslash, which is needed because a lone backslash would be read as the start of an escape sequence rather than as a character in its own right. These sequences are called escape sequences, and they are how strings represent control characters and special characters in a visible, typable form. When you see \n in code, read it as "new line here", and when you see \\, read it as "one backslash character". The backslash is not part of the resulting string; it is an instruction about how to read the character that follows it.

There are two alternative strategies that reduce the need for escaping, and knowing them helps you choose the clearest form for each string. The first is to use a different quote style for the string than for the character inside it: a string containing double quotes can be written in single quotes, as 'She said "hi"', with no escaping needed, and a string containing a single quote, like an apostrophe, can be written in double quotes, as "It's fine". That is often the cleanest solution for a string with one kind of quote in it. The second is to use a template literal, which can contain both kinds of quotes without escaping, needing an escape only for a backtick itself. Escaping remains necessary when a string contains the same kind of quote it is written in, or when it needs a control character like a new line, and in those cases the backslash is the right tool. Choosing the quote style that minimises escaping, and escaping cleanly when escaping is needed, keeps strings readable, which matters because strings are the text humans read, in code as well as on the page.

Check: How do you put a double quote inside a double-quoted string?

Section 35 — Increment and decrement

Increment (++) and decrement (--) are shorthand operators that add one to, or subtract one from, a variable. The expression count++ is equivalent to count = count + 1, and count-- is equivalent to count = count - 1, but shorter and, once familiar, instantly recognisable as "this counter moves by one". They are used wherever a value changes by a single step: counting clicks, keeping score, moving to the next item, tracking attempts. Their brevity is their virtue in loops and counters, where the operation repeats and the shorthand keeps the code compact, but their brevity is also their risk, because they hide a subtlety that beginners should understand before using them in complex expressions.

let count = 0;
count++; // count is now 1

The subtlety is the difference between prefix and postfix forms. When ++ comes before the variable, as in ++count, the variable is incremented first and then its new value is used. When ++ comes after the variable, as in count++, the current value is used first and then the variable is incremented. In a statement on its own, like count++; on a line by itself, the two forms are identical, because the value is not being used by anything else. In an expression where the value is used — assigning the result to another variable, passing it to a function, or embedding it in a larger calculation — the two forms give different results, and that difference is a classic source of off-by-one errors. The expression let next = count++ stores the old value of count in next and then increments count, while let next = ++count increments count first and stores the new value. Both are valid, but they mean different things, and confusing them produces bugs that are hard to spot because the code looks nearly identical.

The safe habit while learning is to use increment and decrement on their own lines, as standalone statements, where prefix and postfix are indistinguishable and the intent is unambiguous. When you later meet them inside expressions — most commonly in loops, where the increment is part of the loop's machinery rather than a value being used — you will understand them in context, and the distinction will matter less because the value is not being captured. That progression, from standalone use to embedded use, mirrors how most developers actually learn these operators: first as a counter's step, then as a loop's step, and only later as a value-producing expression.

A related caution is about readability in team code. Some codebases discourage increment and decrement inside complex expressions precisely because of the prefix-postfix subtlety, preferring the explicit count += 1, which says the same thing without ambiguity. That preference is a matter of style, and you will meet both forms. The important thing is to recognise both, to understand the distinction, and to default to the clearest form for the situation. A counter that steps by one, written on its own line, is clear in any form; a counter embedded in a calculation deserves the explicit form, or at least a comment, so the next reader does not have to pause and work out which value was used.

Check: What does count++ do to count?

Section 36 — Compound assignment

Compound assignment operators combine an arithmetic operation with assignment into a single, shorter form. The most common is +=, which adds a value to a variable and stores the result back in that variable: total += 5 means total = total + 5, with the variable named once instead of twice. The family includes -= for subtraction, *= for multiplication, /= for division, and %= for remainder, each following the same pattern: variable, operator, equals sign, value. With strings, += appends: message += "!" adds an exclamation mark to the end of the message. Compound assignment is not new — everything it does can be written in long form — but it is shorter, and once familiar, it reads more naturally as "add this to the total" rather than as a formula that happens to mention the total twice.

let total = 10;
total += 5; // total is now 15

The readability benefit grows as variable names get longer. The long form shoppingCartTotal = shoppingCartTotal + itemPrice repeats a long name and invites a typo in the repetition, where the two mentions of the name might differ by a character and produce a bug or an error. The compound form shoppingCartTotal += itemPrice names the variable once, so there is nothing to mistype in repetition, and the intent — "add the item price to the cart total" — is visible at a glance. That is why compound assignment is the standard form for accumulation: totals, scores, counts, and strings built piece by piece are always written with +=, and reading code means recognising += as "add to" without translating it into long form first.

Compound assignment also interacts with types like the underlying operator, so the same cautions apply. The expression total += "5" with total holding a number will concatenate rather than add, because += with a string follows the plus operator's string rule, producing "105" from 10 and "5" rather than 15. That is not a flaw in compound assignment; it is the plus operator's behaviour, inherited by its compound form, and the remedy is the same: convert explicitly before accumulating, so the types are what you intend. Similarly, total *= 2 doubles the total, total /= 2 halves it, and total %= 3 replaces it with its remainder on division by 3, each following the arithmetic operator's rules exactly. Understanding the simple operator is understanding the compound form.

There is a stylistic point about when to use compound assignment versus a plain assignment of a computed value. Compound assignment is best when the new value is derived from the old value by one operation: add this, subtract that, append this text. When the new value is a fresh computation that does not build on the old value, plain assignment is clearer: total = price * quantity + tax says the total is computed from scratch, while total += something says the total grows by that something. Using each form for its natural purpose makes code read like a description of what is happening to the value, which is the goal of all the naming and operator choices in this chapter. Short forms are good when they clarify; they are bad only when they obscure, and compound assignment, used for accumulation, clarifies.

Check: What does total += 5 mean in long form?

Section 37 — Operator precedence

When an expression contains more than one operator, JavaScript needs rules for which operation happens first, and those rules are called operator precedence. They match school mathematics for arithmetic: multiplication, division and remainder happen before addition and subtraction, so 2 + 3 * 4 is 14, not 20, because 3 * 4 is calculated first, giving 12, and then 2 is added. Powers happen before multiplication. Comparison operators happen after arithmetic, so a comparison like 2 + 3 > 4 is evaluated as (2 + 3) > 4, which is true, rather than as 2 + (3 > 4), which would be meaningless. Logical NOT happens before AND, and AND before OR, as Section 22 explained. Assignment happens last, so the right-hand side of an assignment is fully calculated before it is stored. These rules form a hierarchy, and every expression is evaluated according to it, whether or not the author was thinking about it.

2 + 3 * 4;     // 14
(2 + 3) * 4;   // 20

Precedence matters most when an expression's intended order differs from the default, or when the default is not obvious to a reader. A formula like price + tax * quantity, read by the precedence rules, multiplies tax by quantity first and then adds price, which is probably not what the author meant if the intention was to tax the whole quantity: that intention needs brackets, as (price + tax) * quantity, or better, as a clearer formulation. The bug in such a case is not a syntax error; the code runs and produces a number, but it is the wrong number, and the wrongness is invisible unless someone checks the calculation against an expected result. That is why precedence errors belong to the logic-error family from the debugging chapter: silent, plausible, and wrong.

The practical response to precedence is not to memorise the hierarchy, though the arithmetic and logical portions are worth knowing. The response is to use brackets whenever the order is not immediately obvious, including when the order is correct but a reader would have to pause to verify it. Brackets cost nothing and save thought at reading time, and they also protect against the author's own future misreading: code you write today will be read by you in six months, when your memory of the intended order has faded, and brackets preserve the intention in the code itself. A good rule of thumb is that if you had to think about precedence while writing an expression, the expression needs brackets, because the next reader will have to think about it too, and brackets remove the need.

There is a secondary benefit to brackets that is easy to overlook: they make code searchable and editable. An expression written as (a + b) * c can be changed to (a + b) * (c + d) by editing one bracketed group, while an expression relying on implicit precedence requires the editor to re-derive the order before changing it safely. Explicit grouping is therefore not just a reading aid but a maintenance aid, and maintenance — changing code safely long after it was written — is most of professional programming. Precedence rules are the background; brackets are the foreground, and the foreground is where clarity lives.

Check: What is 2 + 3 * 4, and why?

Section 38 — Grouping with brackets

Brackets, or parentheses, override precedence and group parts of an expression together, and they are the simplest tool for making an expression mean exactly what you intend. The expression (2 + 3) * 4 calculates 2 + 3 first, giving 5, and then multiplies by 4, giving 20, which differs from 2 + 3 * 4, where multiplication happens first and the result is 14. The brackets do not change the operators or the values; they change the order, and order changes the result. That power, combined with their readability benefit, makes brackets the most used punctuation in programming after the semicolon and the comma, and the habit of using them generously is one of the easiest ways to write clearer code.

Brackets also group ideas, not just operations. The expression (price + tax) * quantity groups "the cost of one item including tax" as a single idea, and then multiplies that idea by the quantity. A reader sees the grouping and understands the structure of the calculation before evaluating any of it: first the per-item cost, then the total. Without brackets, price + tax * quantity groups differently, and the reader must work out the structure from precedence rules rather than from the code's shape. Code that shows its structure is code that can be checked at a glance, and checking at a glance is how errors are caught during review, before they reach a user. Grouping is therefore not just a technical device for overriding precedence; it is a communication device for showing how a calculation is organised.

There are other grouping uses of brackets that follow the same principle. Function calls use brackets to group their arguments, as in console.log(value), where the brackets say "these are the inputs to this function". Conditions in if statements use brackets to group the condition being tested. Mathematical grouping, function arguments, and conditions all share the visual idea of "what is inside these brackets belongs together", and that consistency helps readers navigate code of all kinds. When brackets are nested — an expression inside an expression inside a condition — the grouping becomes a tree, and reading it inside-out, as Section 14 suggested for expressions generally, turns a complicated line into a series of small steps.

A caution about brackets is that they can be overused to the point of noise. An expression like ((2 + 3)) with redundant brackets around a single value adds nothing and slightly obscures the value. The guideline is to use brackets where they change the order or where they clarify the grouping, and to leave them out where the order is already obvious and the grouping adds no information. That judgement improves with practice, and the error to prefer while learning is too many brackets rather than too few, because redundant brackets are harmless while missing brackets produce wrong results. When in doubt, bracket it, and let a future edit remove the bracket if it proves unnecessary. Clarity now is worth more than brevity later.

Check: How do brackets change the result of 2 + 3 * 4?

Section 39 — Declaring several variables

JavaScript allows several variables to be declared in a single statement, separated by commas: let firstName = "Ada", lastName = "Okafor"; creates two variables in one line. That form is compact, and it is common in code written to be terse, but compactness is not the same as clarity, and for most code, one declaration per line is the better choice. The reason is reading: a reader scanning a file looks for variable births, and a birth that shares a line with another birth is easy to miss, especially when the line is long or when the variables have different starting values. One declaration per line gives each variable its own visual space, its own starting value in plain sight, and its own line for a comment if one is needed.

let firstName = "Ada";
let lastName = "Okafor";

There are cases where the single-statement form is reasonable. A group of closely related variables with a shared purpose — coordinates, dimensions, or a set of flags — can read well as a group, particularly when the group is introduced by a comment explaining what the group is for. Some codebases adopt the multi-declaration form as a house style, and consistency with a codebase's style matters more than any individual preference. But those are exceptions to a general guideline, and the guideline exists because of how code is maintained: variables are added, removed, renamed and re-typed over a program's life, and editing a single declaration on its own line is simpler and less error-prone than editing one item in a comma-separated list, where a missing comma or a misplaced semicolon can break the whole statement.

A related form worth knowing is the single declaration with a shared type but no values, or with values only for some variables, which the comma form also allows. That flexibility is another reason to prefer separate lines: a list where some variables have starting values and others do not is harder to scan than separate lines where each variable's state is explicit. The general principle behind this section, and behind much of the advice in this chapter, is that code is read far more often than it is written, and that the cost of an extra line is negligible compared with the cost of a misread variable. Write for the reader, and when the reader is yourself in six months, write for a reader who has forgotten everything about this code. Separate lines, clear names, and explicit starting values are how you do that for declarations. Length is also the key to safe iteration: a loop that visits positions 0 through length minus 1 visits every character exactly once, and a loop that runs to length instead reads one position past the end, giving undefined for the last step. That off-by-one pattern is one of the most common beginner errors, and thinking in terms of length minus 1 prevents it.

Check: Is it better to declare several variables on one line or one per line?

Section 40 — Using a variable before declaring it

Using a let or const variable before its declaration line throws a ReferenceError, and the specific message mentions that the variable cannot be accessed before initialisation. That region of code — after the variable's scope begins but before its declaration line runs — is called the temporal dead zone, a name that sounds dramatic but describes a simple idea: the variable exists in the sense that the language knows its name, but it cannot be touched yet, because its starting value has not been assigned. The dead zone is temporal, not spatial: it is about when, not where. The same variable, used after its declaration line, works perfectly. Used before, it throws.

console.log(score); // Error: Cannot access 'score' before initialization
let score = 0;

The temporal dead zone is a deliberate improvement over the older var behaviour, where using a variable before its declaration gave undefined silently rather than an error. Silent undefined is a bug that hides: code proceeds with a missing value and fails later, far from the cause, with a confusing symptom. An immediate error at the point of use is a bug that announces itself: the line, the variable, and the problem are all in the message, and the fix is to move the use after the declaration or to move the declaration before the use. That immediacy is why let and const were designed this way, and it is a concrete example of the language preferring a loud failure at the point of error over a silent failure downstream.

The practical rule that follows is simple: declare variables at the top of the code that uses them, before any line that reads them. That rule is easy to state and occasionally awkward to follow, because real code is not always organised so neatly — a value may be needed in several places, or a declaration may naturally belong near the code that calculates its starting value. The rule should be read as "before first use", not as "at the very top of the file": a variable declared just above the block that uses it satisfies the rule, and a variable declared at the top of a function satisfies it for the whole function. What violates the rule is a use that precedes its declaration in execution order, and execution order is what matters, not textual position alone, though in straight-line code the two coincide.

When you meet this error, the fix is usually a reordering, not a redesign. Find the declaration, find the use, and put the declaration first in execution order. If the use is inside a function that runs before the declaration line executes — for example, a function called during initialisation that reads a variable declared later — the fix may involve restructuring, such as passing the value as an argument rather than reading it as a shared variable. That restructuring is the subject of the chapter on functions and scope, but the diagnostic step belongs here: a "cannot access before initialisation" error always means order, and order is always fixable by arranging for the declaration to run before the use.

Check: What error do you get from using a let variable before its declaration?

Section 41 — typeof null is "object"

One of JavaScript's most famous quirks is that typeof null returns "object" instead of "null". The reason is historical. In JavaScript's first implementation, values were represented with a type tag in their lowest bits, and objects were tagged with zero. Null, representing no object, was also tagged with zero, because it was implemented as a null pointer, a reference to nothing. When typeof was implemented, it read the tag, saw zero, and reported "object" for null, and that behaviour was never corrected, because correcting it would break existing code that, knowingly or not, depends on it. The web's commitment to backwards compatibility means that a mistake made in ten days of hurried implementation in 1995 is still with us, and will remain with us, because the cost of fixing it exceeds the cost of living with it.

typeof null; // "object" (a historical quirk)

The practical consequence is that typeof cannot be used to test for null. A test like typeof value === "object" is true for objects, for arrays, and for null, so it does not distinguish the cases that matter most: a real object you can use, and null, which will throw an error if you try to read a property from it. The correct test for null is direct comparison: value === null, which is true only for null and false for everything else, including undefined, which is a different value with a different meaning, as Section 5 explained. That direct comparison is simple, exact, and immune to the quirk, and it should be the test you reach for whenever null is a possibility.

The quirk also teaches a broader lesson about the language: not every behaviour in JavaScript is a rational design decision, and some behaviours are accidents preserved by compatibility. A developer who expects every part of the language to be principled will be confused by typeof null, by NaN not equalling itself, and by other historical artefacts. A developer who expects a language shaped by history, with a few rough edges preserved because millions of programs depend on them, will treat those artefacts as facts to know rather than puzzles to solve. That attitude — learn the quirk, use the correct test, move on — is more productive than frustration, and it applies to every language, because every language that survives long enough accumulates quirks.

There is a useful test that combines the lessons of this section with the earlier material. To check whether a value is a usable object, you might test value !== null && typeof value === "object", which excludes null while including objects and arrays, and which reads as "it is an object and it is not null". That compound condition, built from the operators of Sections 19, 21 and 22, is a small example of how the pieces of this chapter combine into practical checks. It also shows why understanding a quirk matters: without knowing that typeof null is "object", a developer writing typeof value === "object" would believe they had excluded null, and would be surprised when null slipped through and caused an error. Knowledge of the quirk turns a plausible test into a correct one.

Check: What does typeof null return, and how should you test for null instead?

Section 42 — Case sensitivity

JavaScript is case-sensitive, which means it treats uppercase and lowercase letters as different characters for the purposes of names. The variables name, Name and NAME are three different variables, and the language will not warn you that they look alike; it will simply treat them as unrelated. The same applies to keywords and method names: the keyword is let, not Let or LET, and the method is toLowerCase, not tolowercase or ToLowerCase. A single wrong capital in a name produces an error — "variable is not defined" if the misspelled name was never declared, or a method-not-found error if the method name is wrong — and those errors, while clear once understood, are confusing the first few times they appear, because the code looks right to a human eye that glosses over capitalisation.

Case sensitivity is a feature of most programming languages, and it exists for a good reason: it allows conventions that carry meaning through capitalisation, like camelCase for variables and PascalCase for constructors, which Section 11 discussed. Those conventions would be impossible if the language ignored case, because the distinction between totalPrice and TotalPrice would vanish, and with it the signal that one is an ordinary variable and the other is a constructor. Case sensitivity is therefore not an arbitrary difficulty; it is the mechanism that makes naming conventions meaningful. But it is also a mechanism that punishes inattention, because a name that differs only in case from another name is easy to mistype and hard to spot.

The defensive habits are straightforward. First, adopt camelCase consistently, so that every multi-word name has a predictable capitalisation pattern, and deviations from that pattern stand out visually. Second, when you meet a "not defined" error for a name you are sure you declared, compare the declaration and the use letter by letter, paying attention to capitals, before assuming a deeper problem; in a large share of cases, the difference is a capital. Third, use an editor that highlights names and offers autocompletion, because autocompletion inserts the exact spelling of a declared name, removing the opportunity for a capitalisation slip. Editors are not a crutch here; they are a tool that compensates for a known human weakness, and professional developers use them for exactly that reason.

There is a related point about reading code written by others. When you see two names that differ only in case, do not assume they are the same variable or a mistake; check their declarations, because they may be deliberately different variables, or one may be a mistake that the original author never noticed. Case sensitivity rewards careful reading, and careful reading is a skill that improves with deliberate practice: slow down on names, check capitals, and let the language's exactness work for you rather than against you. A name is an exact string of characters, and treating it as exact is the first step to debugging the errors it produces.

Check: Are name and Name the same variable in JavaScript?

Section 43 — Reserved words

Some words belong to the language itself and cannot be used as variable names. These are called reserved words, and they include the declarations let, const and var, the control words if, else, for, while, return and switch, the function word function, the class word class, and values like true, false, null and undefined, among others. Trying to declare a variable with one of these names — let let = 5, for example — produces a syntax error, because the language's parser reads the word as its own instruction rather than as your name, and the resulting code cannot be interpreted. The error message will usually name the unexpected token, which is a clue that the word you chose is reserved, even if the message does not use that term.

Reserved words exist because a language needs a vocabulary for its own instructions that cannot be confused with user-defined names. If let could also be a variable name, the line let let = 5 would be ambiguous: is the first let a declaration and the second a name, or is something else meant? Reserving the word removes the ambiguity by fiat: let always means declaration, never a name. That clarity is worth the small cost of losing a few words from the pool of possible names, especially since the pool is effectively infinite and the reserved list is short. Most developers encounter the reserved list only when they accidentally choose a word from it, which happens most often with words that are common in ordinary English but also meaningful in code: class, function, new, default, and switch are frequent examples, because they are natural names for concepts in a program that the language has already claimed.

There are also words that are not strictly reserved but are best avoided as variable names because they are the names of built-in objects or functions: Math, String, Number, console, document, and similar names. Using one of those as a variable name does not produce a syntax error, but it shadows the built-in, hiding it within that scope, which confuses readers and can break code that needs the built-in. A variable called String that holds a user's name, for instance, would prevent that scope from using the String conversion function, and a reader meeting String("42") in that scope would have to work out which String is meant. Avoiding built-in names is a convention rather than a rule, but it is a convention with the same purpose as the reserved list: keep the language's vocabulary distinct from yours, so that code means what it appears to mean.

When a name you pick produces a strange syntax error, check the reserved list before assuming a deeper problem. The fix is simply to choose a different name, usually by adding a qualifier: className instead of class, userFunction instead of function, defaultValue instead of default. That small change resolves the error completely, and the qualified name is often clearer anyway, because it says what kind of class or function or default is meant. Reserved words are a boundary, not an obstacle: they mark the small set of words the language needs for itself, and everything else is yours to name with.

Check: Can you name a variable "function"?

Section 44 — Numbers that look like strings

A value in quotes is a string even if its characters are digits, and that single fact causes more beginner confusion than almost any other in this chapter. The string "10" looks like the number 10, prints like the number 10, and can be mistaken for the number 10 in every casual glance, but it behaves differently in operations that matter. The expression "10" + 5 gives "105", because plus with a string concatenates, while 10 + 5 gives 15, because plus with numbers adds. The expression "10" - 5 gives 5, because minus converts the string to a number, which is correct but inconsistent with plus, and that inconsistency is itself a source of confusion. A value's appearance is not its type, and type, not appearance, determines behaviour.

Values that look like numbers but are strings arrive from the most common sources in front-end work. Form fields give strings, because the user types characters, and the browser does not guess whether those characters represent a number. Addresses give strings, because a URL is text. Data fetched from a server often arrives as strings, because the common data format for the web represents everything as text and leaves interpretation to the receiver. A program that calculates with such values without converting them first will produce concatenation where it expected addition, or will pass a string into a comparison that expects a number, and the resulting bug will be puzzling precisely because the value looks right when printed. The Console shows "10" and 10 similarly enough that the difference can be missed, which is why typeof, from Section 6, is the diagnostic tool for this class of bug: it exposes the type that appearance hides.

The remedy is the habit from Section 25: convert explicitly at the boundary where a value enters your program. When a form field gives you a quantity, convert it with Number() immediately, and store the number, so the rest of the code works with a number and never needs to wonder. When a server gives you a price as a string, convert it on receipt. When a value must be displayed, convert it to a string at the point of display, so the stored value remains numeric. Converting at boundaries, once, is simpler and safer than converting at every use, and it makes the program's types predictable: each variable holds a value of a known type from its birth, and operations on it behave as expected. That boundary discipline also applies to data from an address bar or a stored value: whenever text crosses into your program from outside, treat it as text first, convert it deliberately, and only then calculate with it.

When in doubt, log the type as well as the value, and the trap reveals itself at once.

Check: What does "10" + 5 produce, and why is that a trap?

Section 45 — Choosing between let and const

The choice between let and const is the most consequential small decision in this chapter, because it is made for every variable and because it communicates intent to every future reader. The rule is simple to state: default to const, and switch to let only when you can point to a line where the value must change. A score that increases, a total that accumulates, a current item that advances — those are let, because their values move and the code that moves them is identifiable. A site name, a tax rate, a user's name once known, a reference to an element, a configuration value — those are const, because their values are fixed and the program relies on that fixity. The rule is simple, but its consistent application transforms a codebase, because it labels the moving parts honestly and lets readers trust the parts that are labelled as fixed.

The benefit is not protection against accidental reassignment alone, though that protection is real: a const variable that code tries to reassign throws an error at the point of the mistake, rather than allowing a wrong value to propagate. The benefit is also cognitive. When a reader meets a const variable, they can stop tracking it: its value is the value it was given, everywhere it appears, for its whole life. When a reader meets a let variable, they know to track it: its value at any point depends on which assignments have run, and they must follow those assignments to understand it. A program with many let variables demands more tracking than a program with few, so minimising let variables minimises the reader's burden. That is why the default matters: a codebase where everything is let tells the reader that everything moves, which is false and exhausting, while a codebase where let is reserved for genuinely moving values tells the reader exactly where to pay attention.

There is a common objection from beginners: "What if I use const and later discover the value needs to change?" The answer is that changing the declaration is trivial. Replace const with let at the declaration, and the program now allows the change, with the edit itself documenting that your understanding of the variable changed. That edit is small, deliberate, and reviewable, which is exactly how design decisions should evolve. The opposite direction — starting with let everywhere and later trying to identify which variables never change — is far harder, because it requires proving a negative across the whole program. Starting strict and relaxing deliberately is easier than starting loose and tightening retrospectively, in variable declarations as in most engineering decisions.

Forming this habit early pays compound interest. In larger programs, with functions and objects and asynchronous operations, knowing which values are fixed and which move is essential to reasoning about correctness, and the let-const distinction is the language's built-in way of recording that knowledge. A developer who defaults to const is a developer whose code carries its own documentation about stability, and whose mistakes are caught by the language at the moment they occur. Use const unless you can name the change; use let when you can name it. That sentence, applied consistently, is worth more than any single operator or method in this chapter.

Check: When should you choose let over const?

Section 46 — BigInt for very large integers

Ordinary JavaScript numbers are precise for whole numbers up to about nine quadrillion, which is 2 to the power of 53, minus 1, a limit known as the maximum safe integer. Beyond that limit, integers lose exactness: adding 1 to a very large number may return the same number, because the gap between representable values at that magnitude exceeds 1, and the result is rounded to the nearest representable value. That behaviour is rarely a problem in front-end work, where counts, prices and sizes are far below the limit, but it matters in domains that deal with huge integers: timestamps in nanoseconds, identifiers from some databases, cryptographic values, and calculations in scientific or financial systems that use very large whole numbers. For those cases, JavaScript offers BigInt, a separate type for integers of arbitrary size, written with an n at the end of the literal.

let huge = 12345678901234567890n;

BigInt values are exact at any size, limited only by memory, and they support the same arithmetic operators as numbers: addition, subtraction, multiplication, division, and remainder all work, with division truncating towards zero, as integer division should. That exactness is the point: a BigInt calculation never suffers the rounding that ordinary numbers suffer at large magnitudes, so a program that needs exact huge integers can rely on its results. The trade-off is that BigInt and ordinary numbers do not mix freely: adding a BigInt to a number throws an error, rather than converting silently, because the language refuses to guess whether the result should be exact or approximate. To combine them, you must convert explicitly, with BigInt() for a number to BigInt, or Number() for a BigInt to a number, accepting the possible loss of exactness in the latter direction. That strictness is deliberate: it prevents the silent precision loss that BigInt exists to avoid.

BigInt has its own quirks and limits that are worth knowing even if you rarely use it. Its typeof is "bigint", a distinct type, so type checks must account for it. It cannot represent fractions: a BigInt literal with a decimal point is a syntax error, because BigInt is for integers only. Its mathematical methods are not the Math object's methods, which work on ordinary numbers, so operations like square roots and random values need their own implementations for BigInt. And it is not serialisable to the common data format for the web in the same way as ordinary numbers, which means sending a BigInt to a server or storing it as text requires explicit conversion to a string first. These are not reasons to avoid BigInt; they are the boundaries of its purpose, and a tool used within its purpose is a good tool. Knowing BigInt exists is enough for most developers, until the day a huge identifier misbehaves.

Check: How do you write a BigInt literal, and when would you need one?

Section 47 — Storing a value for later

The whole point of variables is time. A value captured now can be used many lines later, in a different part of the program, without being asked for again, recomputed, or written out a second time. A username typed into a form can be stored when the form is submitted, checked against a rule, and displayed in a greeting, all from the single stored value. A price fetched from a server can be stored when it arrives, displayed in a list, used in a total, and compared against a budget, all without fetching it again. A score earned in a game can be stored when it is achieved and displayed at the end, long after the moment of earning. Whenever a value is needed in more than one place, or at a later time than when it was obtained, storing it in a named variable is the mechanism that makes that possible, and it is the mechanism that turns a sequence of isolated operations into a coherent program.

let userName = "Ada";
console.log(`Welcome back, ${userName}!`);

Storing also enables a program to remember across interactions. A page that only reacted to the current click, with no memory of previous clicks, could not count, could not accumulate a cart, could not track progress through a form, and could not personalise anything. Variables are the page's short-term memory, holding the state that makes continuity possible. That memory is limited to the life of the page: refreshing the page clears variables, which is why longer-term memory requires storage mechanisms like those in the Application panel chapter, but within a page's life, variables are the memory, and managing them well — naming them clearly, choosing let or const correctly, keeping their types stable — is managing the program's state.

There is a discipline to storing that prevents a common beginner problem: storing too much, or storing the same value in several places. When a value is stored in two variables, those variables can drift apart as one is updated and the other is not, and the program then holds two different versions of what should be one truth. The remedy is a single source of truth: store each value once, in one variable, and derive everything else from it when needed, rather than storing copies. A full name can be derived from a first name and a last name when it is needed, rather than stored separately and risked going stale when one part changes. A total can be calculated from its items when it is needed, rather than stored and risked going stale when an item changes. Single source of truth is a principle that grows in importance as programs grow, and it starts here, with the simple decision to store a value once and use it many times, rather than to write it out many times or store it in many places.

Check: Why store a value in a variable instead of writing it out each time?

Section 48 — Reading code with variables

Reading code is a skill distinct from writing it, and it is the skill you will exercise most often as a developer, because you will read far more code than you write: your own old code, colleagues' code, library code, example code. Variables are the thread to follow when reading, because they carry the program's state through its execution. For each variable, track three things: where it is born (its declaration, with its starting value), what value it starts with, and where it changes (each assignment, and the conditions under which those assignments run). A variable whose birth, starting value and changes you understand is a variable whose role you understand, and a program whose variables you understand is a program you can reason about.

The practical technique for tracking a variable is to follow it through the code in order, noting each mention. The declaration tells you the name and the starting value. Each assignment tells you a new value and the circumstances under which it is assigned: is the assignment unconditional, or does it sit inside a condition, a loop, or a function that may or may not run? Each use tells you what the program does with the current value at that point: displays it, compares it, calculates with it, passes it to a function. Building that picture — birth, changes, uses — for the important variables in a piece of code gives you a map of the program's state over time, and that map is usually enough to understand what the program does, even before you understand every line in detail.

When the picture is unclear, the debugging tools from the previous chapter help. Adding a console.log after each change to a variable shows its value at each stage of its life, turning a static reading into a dynamic trace. Setting a breakpoint and watching the variable in the Scope pane shows its value at a particular moment, with the call stack showing how the program arrived there. These are not just debugging techniques; they are reading techniques, ways of making a program's behaviour visible when static reading is not enough. A developer who reads code by tracing variables, with logs or breakpoints when needed, will understand unfamiliar code faster than a developer who reads line by line hoping for a holistic impression, because variables are the structure that holds the lines together.

There is also a habit of reading that applies to your own code as you write it: after writing a block, re-read it as a stranger, following each variable from birth to use, and ask whether the story makes sense. Does every variable get a sensible starting value before it is read? Does every change happen for a reason a reader can see? Does any variable change in a place that would surprise a reader? That re-reading, done before the code is considered finished, catches a large share of variable bugs — uninitialised reads, stale values, accidental reuse of a name — while they are still cheap to fix. Reading code well is not a passive activity; it is an active tracing of values through time, and variables are the trail to follow.

Check: What three things should you track for each variable when reading code?

Section 49 — Common beginner mistakes with values

Every beginner makes the same handful of mistakes with values and variables, and knowing them in advance does not prevent you from making them, but it does mean you will recognise them quickly when they appear, which is almost as good. The first is mixing up assignment and equality: writing if (x = 5) when you meant if (x === 5), which assigns instead of testing and makes the condition always pass, as Section 12 explained. The symptom is a condition that never fails, and the fix is a second equals sign and a careful re-read of the condition. The second is forgetting quotes around text: writing let name = Ada instead of let name = "Ada", which the language reads as a variable reference rather than a string, producing a "not defined" error for a name you never declared. The symptom is an error naming the word you meant as text, and the fix is quotes.

The third is using a variable before declaring it, which produces the temporal dead zone error from Section 40, and whose fix is reordering so the declaration runs first. The fourth is trusting the plus operator to add when one side is a string, producing concatenation instead of addition, as Sections 16 and 24 explained, whose symptom is a total that looks like two numbers joined together, and whose fix is explicit conversion before calculating. The fifth is forgetting that a string method returns a new string, and calling a method without storing its result, so the original string appears unchanged, as Section 30 explained. The sixth is comparing decimals for exact equality, which fails because of the approximation from Section 31, and whose fix is a tolerance comparison or whole-unit arithmetic for money. Each of these mistakes has a clear symptom, a clear diagnosis, and a clear fix, and none of them requires deep knowledge to resolve once the pattern is recognised.

The deeper lesson of this list is that beginner mistakes are not random; they cluster around the boundaries between concepts — assignment versus equality, text versus names, declaration versus use, strings versus numbers, methods versus mutation, exact versus approximate. Boundaries are where understanding is tested, because each side of a boundary behaves differently and the code must respect the difference. A developer who knows where the boundaries are, and what changes across each one, will make fewer mistakes and will diagnose the ones they do make faster. That is why this chapter has spent so much time on distinctions that might seem pedantic: the distinctions are the map, and the mistakes happen at the borders. Each pattern on this list becomes easier to spot with practice, and spotting it quickly is the real skill.

Check: Name three common beginner mistakes from this section.

Section 50 — Chapter 16 in one paragraph

Values are the data your programs handle — numbers, strings, booleans, undefined and null — and variables are named places to keep them, declared with let when the value will change and const when it will not, named in camelCase so that readers can follow them, and tracked from birth through every change to understand what a program does. Operators combine and compare those values: arithmetic for calculation, comparison for questions, logic for compound rules, with precedence and brackets determining the order, and with strict equality preferred because it does exactly what it appears to do. Types can be checked with typeof and converted explicitly with Number, String and Boolean, which is safer than relying on quiet coercion, and a handful of quirks — decimal approximation, typeof null, string-plus-number, NaN's refusal to equal itself — are worth knowing now so that they never surprise you later. Hold those ideas together, and the next chapter's functions will make sense as the natural next step: named blocks of code that take values in, work with them through variables, and return values out. That summary is also a map for revision: any idea you cannot recall from it is a section worth re-reading before you move on. Return to the sections on coercion and on let versus const first, because those two ideas cause the most debugging time when they are shaky. Look back over the chapter and notice how its ideas stack. Values are the raw material, and types are the rules for what each value can do: numbers calculate, strings describe, booleans decide, and undefined and null mark two different kinds of absence. Variables give those values names and lifetimes, with let for values that move and const for values that stay, and with camelCase names that let readers follow them. Operators combine values into new ones, and the combination rules — precedence, grouping, strict equality, truthiness, and coercion — determine what each combination means, which is why this chapter spent so much time on distinctions that can seem pedantic in isolation. Those distinctions are where bugs live: at the boundary between assignment and equality, between a string and a number, between a value and its type, between an exact integer and an approximate decimal. Knowing the boundaries, and converting explicitly when crossing them, is the practical skill the chapter builds, and it is a skill that transfers to every later chapter, because every data structure, every function, and every fetched result is ultimately values in variables, combined by operators, compared by conditions, and checked by a developer who knows what type each value is. If you remember one sentence from this chapter, remember that a variable holds a value of a type, and that most surprises come from the type being different from the one you assumed.

Check: In your own words, summarise when to use let and when to use const.

Practical task

Build a small page with a script that: stores your name, your age, and whether you are a student in three well-named variables (using const where the value will not change and let where it might); builds a greeting sentence with a template literal; calculates the year you will turn 40; and prints the greeting, the year, and the type of each variable to the Console with console.log and typeof. Open the page, check the Console output matches what you expect, then change one value and confirm the output changes with it. Then deliberately introduce one mistake from Section 49 — for example, use a variable before declaring it, or add a string to a number without converting — observe the error or wrong result in the Console, fix it, and write down in a sentence what the mistake taught you about that boundary between concepts.

Chapter questions

  1. What is the difference between undefined and null, and when would you use each?
  2. Explain why "5" + 3 gives "53" but 5 + 3 gives 8.
  3. When should you use let, and when should you use const? Give an example of each.
  4. What does 0.1 + 0.2 evaluate to, and how would you handle money safely in JavaScript?
  5. Write code that stores a first name and a last name, joins them into a full name with a template literal, and prints the full name and its length.