Values, Variables, and Equality in JavaScript
Think of a JavaScript variable as a luggage tag rather than a suitcase. The tag carries a name and you can tie it to any bag you like, swapping bags without ever changing the tag. The type belongs to the bag, never to the tag, and that single arrangement explains most of what follows.
This guide covers the pieces you use in every line of real code: declaring values, the primitive types, how JavaScript converts between them, the two equality operators, what counts as true in a condition, the difference between null and undefined, and the two operators that handle missing data cleanly.
Declaring Values with let and const
There are two declarations worth using. const ties the tag to one bag permanently, and let lets you retie it later:
let visitCount = 0;
visitCount = visitCount + 1;
console.log(visitCount);
const siteName = "CodeWalkers";
siteName = "Other"; // node output:
// 1
// TypeError: Assignment to constant variable. Notice that the error arrives while the code runs, not before it. The first two lines executed and printed happily, and the program only stopped when it reached the reassignment. The engine does check the whole file for syntax errors first, but a bad reassignment is not one of those.
The word const is doing less than it looks like it does. It locks the tag to one bag, and says nothing about what is inside the bag:
const settings = { theme: "dark" };
settings.theme = "light";
console.log(settings.theme); // node output:
// light The object was declared const and its contents changed anyway, because the binding never moved. If you genuinely need the contents locked, Object.freeze(settings) does that, though it only freezes the top level.
The practical convention on most teams is short. Reach for const first, switch to let the moment you find yourself needing to reassign, and treat a file full of let as a hint that some values are being reused where a new name would read better.
The Primitive Types
JavaScript has seven primitive types, plus objects for everything else. The typeof operator reports which one a value is:
console.log(typeof "hello");
console.log(typeof 42);
console.log(typeof true);
console.log(typeof undefined);
console.log(typeof null);
console.log(typeof Symbol("id"));
console.log(typeof 9007199254740993n); // node output:
// string
// number
// boolean
// undefined
// object
// symbol
// bigint Six of those answers are what you would expect. The seventh, typeof null returning "object", is a bug from the first version of the language in 1995 that was never fixed because fixing it would break existing sites. Every JavaScript engine reproduces it deliberately.
Two of these types deserve a note. There are two numeric types, and number is the one you will use for nearly everything, covering integers and decimals alike as 64-bit floating point. That format is why 0.1 + 0.2 comes out as 0.30000000000000004.
BigInt, written with a trailing n, is the second numeric type, for whole numbers too large for that format to hold exactly. The two do not mix: most arithmetic combining a number and a BigInt throws a TypeError rather than converting.
Objects and arrays are compared by identity, so two separate objects with identical contents are never equal to each other. That surprises people the first time they compare two arrays. Primitives compare by value under ===, with two documented oddities: NaN is not equal to itself, and -0 and 0 are equal.
Type Coercion
When an operator gets types it did not expect, it usually converts rather than complains. This is called coercion, and it is the source of the language's strangest reputation. A few combinations do throw instead, notably BigInt mixed with number and any attempt to convert a symbol to a number:
console.log("5" + 3);
console.log("5" - 3);
console.log(1 + true);
console.log(0.1 + 0.2); // node output:
// 53
// 2
// 2
// 0.30000000000000004 The + operator is the odd one out. Each side is converted to a primitive first, and if either result is a string it concatenates, so "5" + 3 becomes the string "53". The other arithmetic operators convert to a numeric type instead, which is why "5" - 3 quietly gives you 2.
The luggage-tag picture explains where this bites. A form field always hands you a string, so a quantity read from an input and added to a running total concatenates instead of adding. Convert at the boundary with Number(value) and the rest of your code works on the type it expects.
Converting on purpose is a two-line habit worth forming, and the conversion functions have edges of their own:
console.log(Number("49.5"));
console.log(Number("49.5kg"));
console.log(Number(""));
console.log(parseFloat("49.5kg")); // node output:
// 49.5
// NaN
// 0
// 49.5 On strings, Number is strict: the whole trimmed string must be valid numeric syntax or the result is NaN, and an empty or whitespace-only string becomes 0. It converts non-strings too, turning true into 1 and null into 0.
parseFloat and parseInt are lenient and read as far as they can. That is what you want for "49.5kg" and dangerous for a field that should have been numeric all along.
The floating point result in the earlier example is not coercion, but it belongs to the same family of surprises. Money and other exact quantities are safest handled in whole cents as integers, then formatted for display at the end.
Comparing Values with Double and Triple Equals
JavaScript ships two equality operators. === never converts, and == may convert one side when the two types differ, though same-type operands take the strict path and null against undefined has a rule of its own:
console.log(1 == "1");
console.log(1 === "1");
console.log(null == undefined);
console.log(null === undefined);
console.log(0 == "");
console.log(NaN === NaN); // node output:
// true
// false
// true
// false
// true
// false The default is ===, because it means what it appears to mean. The conversion table behind == is real, documented, and consistent, but almost nobody carries it in their head, so a comparison that depends on it is a comparison the next reader has to look up.
There is one == case worth keeping. Writing value == null is true for both null and undefined, which is usually exactly the question you are asking when you check whether something is missing.
The last line is not a typo. NaN, the value you get from arithmetic that has no numeric answer, is not equal to itself under either operator. Test for it with Number.isNaN(value).
Truthiness
An if statement has to reach a yes or no answer about any value you hand it, so every value is either truthy or falsy. The falsy list is short and worth memorising, because everything else is truthy:
for (const value of [0, "", null, undefined, NaN]) {
console.log(Boolean(value));
}
console.log(Boolean("0"));
console.log(Boolean([]));
console.log(Boolean({})); // node output:
// false
// false
// false
// false
// false
// true
// true
// true The falsy values are false, 0, -0, 0n, "", null, undefined, and NaN. That is the whole language list. Everything else, including the string "0", an empty array, and an empty object, passes an if check.
Browsers add one historical exception that no other object shares. The legacy document.all is an object, yet it is falsy and loosely equal to undefined, kept that way so old sites keep working.
The empty array is the one that catches people. Checking if (results) to find out whether a search returned anything is always true, because an empty array is still an array. Ask about the length instead, with if (results.length > 0).
null Against undefined
JavaScript has two ways to say "no value". The useful convention is that undefined means nobody set this and null means somebody set it to nothing on purpose, though the language enforces none of that: code can assign undefined, and an API can hand you null nobody chose:
const user = { name: "Ada", nickname: null };
console.log(user.nickname);
console.log(user.email);
console.log(JSON.stringify(user)); // node output:
// null
// undefined
// {"name":"Ada","nickname":null} The engine produced undefined on its own for the property that was never there. It also does this for a function parameter you did not pass, and for a function that returns nothing.
The last line shows a practical difference. JSON.stringify keeps a null property and drops an undefined one entirely, so the choice changes what actually reaches your API. Most teams settle on one convention: null for a field that exists and is deliberately empty, and undefined left to the engine.
Nullish Coalescing and Optional Chaining
Two operators exist specifically for the missing-value problem. ?? supplies a fallback, and ?. stops a property lookup safely partway down:
const settings = { pageSize: 0, theme: null };
console.log(settings.pageSize || 20);
console.log(settings.pageSize ?? 20);
console.log(settings.theme ?? "dark");
console.log(settings.profile?.avatar); // node output:
// 20
// 0
// dark
// undefined The first two lines are the whole argument for ??. The || operator falls back on any falsy value, so a deliberate page size of 0 was overwritten with 20. The ?? operator only falls back on null and undefined, so the real value survived.
The last line reads a property of a property that does not exist. Without ?. that line throws a TypeError; with it, the chain short-circuits and evaluates to undefined. The same syntax works on function calls with obj.method?.() and on indexes with list?.[0].
Optional chaining is easy to over-apply. Every ?. you add is a claim that a value legitimately might be missing, so scattering them through code where the value is always present hides real bugs behind a silent undefined.
Pitfalls and Debugging
TypeError: Cannot read properties of undefined (reading 'x'). The most common runtime error in JavaScript, and the message names the property you tried to read, not the thing that was missing. Read it as "something before .x was undefined", then log the chain one step at a time until you find which step returned nothing.
ReferenceError: x is not defined. Different error, different cause. This one means the name itself does not exist in scope, usually from a typo, a missing import, or browser-only code such as document running in Node.
ReferenceError: Cannot access 'x' before initialization. A close relative, and a different fix. It means a let, const, or class binding was reached before its declaration ran, which is usually a read above the declaration line but also happens through a function called too early or an import cycle.
Numbers arriving as strings. A text input's value, a URLSearchParams entry, and a Node process.env entry are strings or absent, so a total that reads "1020" instead of 30 means a concatenation happened. Convert once at the boundary with Number(value), and check the result with Number.isNaN before trusting it.
Comparing two objects or arrays. [1, 2] === [1, 2] is false, because each literal creates a separate value and the comparison is by identity. Compare the fields you actually care about, or compare serialised forms when the key order is under your control.
A falsy check that was meant to be a missing check. if (!count) treats a real count of zero as absent, and if (!name) treats an empty string the same way. When the question is "was this provided", ask if (count == null) instead.
Frequently Asked Questions
Should you ever use double equals?
One case is genuinely useful: comparing against null with == also matches undefined, which is often what you mean when checking whether a value is missing. Everywhere else, use === so the comparison does not depend on conversion rules you have to remember.
Why does 0.1 plus 0.2 not equal 0.3?
JavaScript numbers are 64-bit floating point values, and 0.1 and 0.2 have no exact binary representation. The sum comes out as 0.30000000000000004. Compare money and other exact quantities by working in whole cents, or compare within a small tolerance rather than exactly.
Is var still allowed?
Yes, and it will keep working, since the language does not remove old syntax. It behaves differently from let: it is function-scoped rather than block-scoped and it can be redeclared. New code uses const by default and let when a value must change.
When should you use ?? instead of ||?
Use ?? whenever 0, an empty string, or false are legitimate values. The || operator falls back on any falsy value, so a page size of 0 or a search box left empty silently becomes the default. The ?? operator only falls back on null and undefined.
Related
- JavaScript Language Basics for the full topic overview
- Functions, Callbacks, and Closures for how functions capture the variables declared here
- Objects, Arrays, and Destructuring for the values that are not primitives
- JavaScript for the language guide and the wider learning path
Sources
-
[1]
JavaScript Data Types and Data Structures(developer.mozilla.org)
-
[2]
Equality Comparisons and Sameness(developer.mozilla.org)
-
[3]
Nullish Coalescing Operator(developer.mozilla.org)
Read Next
Declarations against expressions, arrow functions and what this binds to, default and rest parameters, callbacks, closures, and functions that take or return functions.
Object literals and property access, the array methods worth knowing, destructuring, spread and rest, when Map and Set beat a plain object, and copying without mutating.
The four pieces of JavaScript every other page assumes: values and equality, functions and closures, objects and arrays, and modules that split code across files.