JAVASCRIPT / VALUES, TYPES, AND COERCION
NaN and unreliable numeric results
Recognize where NaN comes from, detect it with Number.isNaN instead of equality, and spot float results that are wrong without ever becoming NaN.
What you will learn
- Detect NaN with Number.isNaN or the x !== x self-comparison, never with === NaN
- Trace a NaN backwards to the first operation that produced it, not where it printed
- Compare fractional results with a tolerance like Number.EPSILON instead of ===
- Explain why includes(NaN) is true while indexOf(NaN) returns -1
Understanding NaN and unreliable numeric results
NaN is the value JavaScript produces when an arithmetic operation has no meaningful numeric answer: 0/0, Math.sqrt(-1), Infinity - Infinity, or multiplying by a string that could not be read as a number. Nothing is thrown, and the result's type is number, which is why a NaN can travel a long way through a program before anyone notices. Every arithmetic operation involving NaN returns NaN, so one unusable input at the top of a calculation quietly ruins every total, average, and percentage downstream. Treat NaN as a marker riding along with the result, not as an error the language will report for you.
NaN is the only JavaScript value that is not equal to itself. IEEE 754 defines comparisons with NaN as unordered, so NaN === NaN, NaN < 1, and NaN >= 1 are all false, while NaN !== NaN is true. That is why you cannot test for it by comparison: Number.isNaN(x) asks whether x is that specific value, while the global isNaN(x) converts x to a number first and then answers, which makes isNaN('4 2') true and isNaN('') false. The same split appears in collections, where indexOf uses === and never finds NaN, but includes, Object.is, Set members, and Map keys use a comparison that deliberately treats NaN as matching itself.
Wrong numeric results do not always announce themselves as NaN. Numbers are 64-bit binary floats with a 53-bit significand, so a decimal like 0.1 has no exact representation and 0.1 + 0.2 evaluates to 0.30000000000000004, which is the exact sum of the values actually stored rather than of the ones you typed. The same limit means only integers up to 2**53 - 1 (Number.MAX_SAFE_INTEGER) are individually representable, so 9007199254740992 + 1 === 9007199254740992 is true and a naive counter or ID can stop advancing. Compare fractional results within a tolerance, and keep money in integer cents or large whole numbers in BigInt so the arithmetic stays exact.
const price = Number("12.50");
const qty = Number("two"); // parsing failure, not an exception
const total = price * qty;
console.log(total);
console.log(typeof total);
console.log(total === total);
console.log(Number.isNaN(total));
const withShipping = total + 5; // NaN keeps spreading
console.log(withShipping);
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);NaN is a real number-typed value that marks a failed computation, spreads into every later operation, and refuses to equal itself, so it must be detected by identity rather than by comparison.
Worked examples
isNaN against Number.isNaN
Shows that the global isNaN answers a different question than Number.isNaN, and why that matters for validation.
console.log(isNaN("42"), Number.isNaN("42"));
console.log(isNaN("4 2"), Number.isNaN("4 2"));
console.log(isNaN(""), Number.isNaN(""));
console.log(isNaN(undefined), Number.isNaN(undefined));
console.log(isNaN(NaN), Number.isNaN(NaN));Example explained
Line 1isNaN("4 2") is true because the global isNaN converts its argument first, and "4 2" converts to NaN.
Line 2Number.isNaN("4 2") is false: the string itself is not the NaN value, no matter what it would convert to.
Line 3isNaN("") is false because an empty string converts to 0, so a validator built on isNaN accepts blank input.
Line 4Only the final line agrees, since NaN is the one argument where 'is NaN' and 'would become NaN' coincide.
Finding NaN inside an array
Demonstrates which search operations can locate a NaN element and which are structurally unable to.
const readings = [1, NaN, 3];
console.log(readings.indexOf(NaN));
console.log(readings.includes(NaN));
console.log(readings.findIndex(Number.isNaN));
console.log(Object.is(NaN, NaN));Example explained
Line 1indexOf compares with ===, and NaN === NaN is false, so it can never match the element at index 1.
Line 2includes uses SameValueZero, a comparison defined to treat NaN as equal to NaN, so it reports true.
Line 3findIndex works because Number.isNaN inspects each element by identity instead of comparing it to a target.
Line 4Object.is(NaN, NaN) is true as well; it differs from includes only in that it also distinguishes 0 from -0.
A wrong answer that is not NaN
Accumulating a tenth ten times lands near 1 but not on it, so equality fails while a tolerance check succeeds.
let sum = 0;
for (let i = 0; i < 10; i++) {
sum += 0.1;
}
console.log(sum);
console.log(sum === 1);
console.log(Math.abs(sum - 1) < Number.EPSILON);
console.log(Number(sum.toFixed(2)) === 1);Example explained
Line 10.1 has no exact binary representation, so each += adds a value slightly different from one tenth.
Line 2The accumulated error lands on 0.9999999999999999, which is why sum === 1 is false.
Line 3Math.abs(sum - 1) is 2**-53, just below Number.EPSILON (2**-52), so the tolerance comparison passes.
Line 4toFixed(2) produces the string "1.00" for the comparison, but sum itself still holds the drifted value.
Important notes
NaN and Infinity are different failures: 0/0 is NaN but 1/0 is Infinity, and Number.isNaN(Infinity) is false, so use Number.isFinite when you want to reject both.
Number.isNaN and Number.isFinite never coerce their argument, which is why Number.isFinite("5") is false; convert first, then test.
Common mistakes
Guarding with `if (total === NaN)`: the comparison is always false, so the branch never runs and the text 'NaN' is rendered to the user.
Using global isNaN as an input check: isNaN("") and isNaN(" ") are both false, so a blank field is accepted, converts to 0, and the order is silently priced for zero items.
Assuming toFixed(2) repaired a floating-point total; it only rounds the displayed string, so the stored value keeps drifting and two separately computed totals disagree by a cent.
Try it yourself
Change, predict, then run
In the browser console, write toTotal(items) that maps each item's qty through Number, returns null if any converted value fails Number.isFinite, and otherwise sums them. Run it on [{qty:"2"},{qty:""}] and [{qty:"2"},{qty:"x"}], and explain which case your guard misses and why.
Open the JavaScript workspaceCheck your understanding
A quantity field is left blank, and the code runs: const qty = Number(input); if (isNaN(qty)) { showError(); } const total = qty * price; What actually happens?
- qty is NaN, so showError runs and total is never computed.
- isNaN throws a TypeError because an empty string is not a number.
- qty is 0, isNaN(0) is false, so no error is shown and total quietly becomes 0.
- qty is 0 but isNaN still returns true, because the field was empty.
Show answer
An empty string converts to 0, not NaN, so isNaN reports false and the missing quantity flows into the multiplication as a legitimate zero. The first option is tempting because blank input feels invalid, but only text containing non-numeric characters converts to NaN; catching an empty field requires checking the raw input before conversion, not a NaN test afterwards.