Values, Variables, and Equality in JavaScript
JavaScript variables name values, and those values carry their types. Think of a 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.
These JavaScript language basics turn up constantly in 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.
When Should You Use let or const?
Use const when you will not reassign a variable and let when you will. In the luggage-tag analogy, const fixes the tag to one bag; let allows you to retie it:
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's contents changed despite its const declaration, because the binding never moved. If you 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.
What Are JavaScript's Primitive Types?
JavaScript's seven primitive types are string, number, bigint, boolean, undefined, symbol and null. The typeof operator identifies most of them, with a historical exception for null:
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 exception, 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.
How Does JavaScript Type Coercion Work?
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. An input element's value property is 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.
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.
What Is the Difference Between == and ===?
JavaScript's === checks equality without conversion. The == operator can convert values of different types before comparing them, while same-type operands follow strict equality. It also treats null and undefined as equal:
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.
Object.is() offers another comparison rule: it treats NaN as equal to itself and distinguishes -0 from 0. Neither Object.is() nor === compares object contents.
NaN, the value you get from arithmetic that has no numeric answer, is not equal to itself under == or ===. Test for it with Number.isNaN(value).
Which JavaScript Values Are Truthy or Falsy?
JavaScript’s falsy values are false, 0, -0, 0n, an empty string, null, undefined, and NaN. Browsers also retain the legacy falsy object document.all. Other values are 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).
How Do null and undefined Differ?
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 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.
How Do ?? and ?. Handle Missing Values?
JavaScript’s ?? supplies a fallback when its left operand is null or undefined. Optional chaining, ?., stops a property access or call when the value it checks is null or undefined, returning undefined:
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'). This 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 care about, or use a deep-comparison function with rules that suit your data. JSON serialization can lose values and is not a general equality test.
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
When should you use JavaScript double equals (==)?
JavaScript double equals (==) is useful for a deliberate null check because value == null also matches undefined. Prefer === for ordinary comparisons so implicit conversion does not change the result. The legacy browser object document.all is an unusual exception to the null-check rule.
Why does 0.1 + 0.2 not equal 0.3 in JavaScript?
JavaScript Number values use binary floating point, which cannot represent 0.1 or 0.2 exactly. Their sum is 0.30000000000000004. For approximate measurements, compare with a tolerance suited to the scale. For money, use carefully rounded minor units within the safe-integer range or a decimal arithmetic library.
Is var still valid in JavaScript?
JavaScript still supports var. Unlike let and const, var is function-scoped rather than block-scoped and permits redeclaration. Prefer const for bindings that stay fixed and let for variables you reassign in new code.
When should you use JavaScript ?? instead of ||?
Use JavaScript ?? for a default that should apply only to null or undefined. The || operator also replaces other falsy values, including 0, false and an empty string. Choose ?? when those are valid inputs you want to preserve.
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.