A Number That Always Returns the Same Thing
Today we’ll look at the NaN type, which in JavaScript is identified as type number.
> typeof NaN
'number'
We get the type number in response.
Since something is a number, logic suggests we can perform mathematical operations on it.
So let’s try adding something to it or checking its maximum or minimum value.
> NaN + 1
NaN
> NaN - 1
NaN
> Math.max(NaN)
NaN
> Math.min(NaN)
NaN
As you can see, after adding, subtracting, checking the maximum and minimum values - we always get the same result.
If that’s the case, why do we need such a value?
To try to explain this, let’s look into Firefox or V8 to find the usage and implementation of NaN.
// Firefox
bool isNaN() const { return isDouble() && std::isnan(toDouble()); }
// V8
if (IsMinusZero(value)) return has_minus_zero();
if (std::isnan(value)) return has_nan();
Looking at the code of example browsers, the std::isnan method from the standard library is used to check for NaN, which may already suggest to us that this is something that appeared independently of JavaScript.
And indeed, looking historically, the first standardization of NaN appeared in 1985 and was given the number IEEE 754.
From JavaScript to Hardware Level
Armed with this knowledge, let’s write a simple program in C, where based on what we found in the browser code, we’ll check how NaN behaves.
> NaN !== NaN
true
> 0 / 0
NaN
#include <math.h>
#include <stdint.h>
#include <stdio.h>
int main() {
double x = 0.0 / 0.0;
if (x != x) {
printf("NaN is not the same\n");
}
if (isnan(x)) {
printf("x is NaN\n");
}
uint64_t bits = *(uint64_t*)&x;
printf("NaN hex: 0x%016lx\n", bits);
return 0;
}
The result is the same as in JavaScript!
NaN is not the same
x is NaN
NaN hex: 0xfff8000000000000
We already know that we encounter NaN in other programming languages as well
#Python
import math
nan = float('nan')
print(nan != nan) # True
print(nan == nan) # False
print(math.isnan(nan)) # True
//C++
#include <iostream>
#include <cmath>
int main() {
double nan = NAN;
std::cout << (nan != nan) << std::endl; // 1 (true)
std::cout << (nan == nan) << std::endl; // 0 (false)
std::cout << std::isnan(nan) << std::endl; // 1 (true, proper way)
return 0;
}
//Rust
fn main() {
let nan = f64::NAN;
println!("{}", nan != nan); // true
println!("{}", nan == nan); // false
println!("{}", nan.is_nan()); // true (proper way)
}
but we still don’t know what it’s for.
And since we don’t know, let’s generate assembly code for our modest program (let’s skip the prologue and stack frame initialization).
# =====================================
# double x = 0.0 / 0.0;
# =====================================
pxor xmm0, xmm0 # xmm0 = 0.0
divsd xmm0, xmm0 # xmm0 = 0.0 / 0.0 = NaN
movsd QWORD PTR -8[rbp], xmm0 # x = NaN
# =====================================
# if (x != x) {
# =====================================
movsd xmm0, QWORD PTR -8[rbp] # xmm0 = x
ucomisd xmm0, QWORD PTR -8[rbp] # compare x with x (sets PF=1 for NaN)
jnp .L2 # skip if NOT NaN (PF=0)
# NaN detected - code here
.L2:
# =====================================
# if (isnan(x)) {
# =====================================
movsd xmm0, QWORD PTR -8[rbp] # xmm0 = x
ucomisd xmm0, QWORD PTR -8[rbp] # compare x with x (sets PF=1 for NaN)
jnp .L3 # skip if NOT NaN (PF=0)
# NaN detected - code here
.L3:
For those who haven’t had contact with assembly - what’s worth noting for us is the xmm0 register, which performs operations on floating-point numbers.
Which is logical: we want to perform an operation on numbers, the CPU operates on numbers, so it will be fastest to do this in registers specifically designed for this purpose!
We can also see the ucomisd instruction, which is responsible for setting a flag when it detects NaN.
What conclusion can we draw from this? NaN is implemented at the hardware level, not at the JavaScript abstraction level.
So - rewriting the program in assembly to avoid unnecessary abstractions, let’s examine its execution result:
#include <stdio.h>
#include <stdint.h>
int main() {
double x;
uint64_t bits;
__asm__ (
// double x = 0.0 / 0.0;
"pxor xmm0, xmm0\n\t" // xmm0 = 0.0
"divsd xmm0, xmm0\n\t" // xmm0 = 0.0 / 0.0 = NaN
// Save results
"movsd %0, xmm0\n\t" // x = NaN
"movq %1, xmm0\n\t" // bits = *(uint64_t*)&x
: "=m" (x), "=r" (bits)
:
: "xmm0"
);
int is_not_equal;
__asm__ (
// if (x != x)
"movsd xmm0, %1\n\t" // xmm0 = x
"ucomisd xmm0, %1\n\t" // compare x with x → PF=1 for NaN
"setp al\n\t" // al = (x != x)
"movzx %0, al\n\t" // is_not_equal = al
: "=r" (is_not_equal)
: "m" (x)
: "xmm0", "al"
);
if (is_not_equal) { // if (x != x)
printf("NaN is not the same\n");
}
int is_nan_result;
__asm__ (
// if (isnan(x))
"movsd xmm0, %1\n\t" // xmm0 = x
"ucomisd xmm0, %1\n\t" // compare x with x → PF=1 for NaN
"setp al\n\t" // al = isnan(x)
"movzx %0, al\n\t" // is_nan_result = al
: "=r" (is_nan_result)
: "m" (x)
: "xmm0", "al"
);
if (is_nan_result) { // if (isnan(x))
printf("x is NaN\n");
}
printf("NaN hex: 0x%016lx\n", bits);
return 0;
}
The result?
NaN is not the same
x is NaN
NaN hex: 0xfff8000000000000
The program’s output is the same as with high-level C.
We already know that NaN is natively implemented, so let’s look at the ucomisd instruction.
"ucomisd xmm0, %1\n\t" // compare x with x → PF=1 for NaN
ucomisd - or Unordered Compare Scalar Double-precision floating-point. This wonderful instruction saved time and nerves for programmers on the x86 architecture, because at the CPU level it already checks whether the result of operations on numbers is correct or not.
NaN !== NaN
The main reason was to provide programmers with a way to detect NaN using the x != x test in times when the isnan() function didn’t yet exist in programming languages.
From a logical point of view, this makes a lot of sense, because a non-value cannot equal a non-value.
This is intentional design, not a bug.
typeof NaN === “number”
NaN is part of the numeric system (IEEE 754), not a separate type. It’s a special numeric value signaling a mathematical operation error.
IEEE 754-1985: Standard for Binary Floating-Point Arithmetic
- Published: 1985
- Author: William Kahan (UC Berkeley) + IEEE committee
- Defines: NaN, Infinity, denormalized numbers, rounding modes
Key decisions:
NaN !== NaN(alwaysfalsewhen comparing for equality)- Exponent =
0x7FF, mantissa ≠ 0 - Quiet NaN (qNaN) - propagates through operations without signaling an exception
- Signaling NaN (sNaN) - generates an exception on first use in an operation
NaNpropagation (any operation withNaN→NaN)
NaN is a Number, But What Kind?
You might be surprised why floating-point number registers are used for dividing 0/0.
Operations on number type values in JavaScript are represented as double-precision floating-point numbers (double) to perform operations on them according to the IEEE 754 standard.
In integer operations, division by zero is an unambiguous error. However, in floating-point numbers, we have many cases that can lead to undefined results:
0.0 / 0.0→NaN∞ - ∞→NaN0 * ∞→NaNsqrt(-1)→NaN
Without the IEEE 754 standard, each hardware manufacturer dealt with these situations differently, resulting in enormous code portability problems.
1994: Pentium FDIV Bug
A bug in Pentium’s floating-point division - some divisions gave incorrect results. It wasn’t a problem with NaN, but it showed the importance of precise IEEE 754 implementation.
Intel replaced millions of processors, which cost the company $475 million.
NaN as the Savior of Programmers
We learned that NaN is set at the hardware level, but what was there before NaN?
Before the IEEE 754 standard (1985), each hardware manufacturer did it their own way, which usually meant that operations like 0/0 ended in a crash and program termination.
This required very defensive programming from developers. Imagine you’re flying in an airplane, and in the control system, a programmer didn’t anticipate 0/0 - the instruction executes on the CPU and crashes the entire program due to a Division Error!
Intel and other manufacturers were fed up with the chaos resulting from programs behaving differently on different architectures.
NaN (Not a Number)
We can ask ourselves why a special value instead of another solution.
Let’s consider different options:
Option A: Division Error → CRASH (existing before IEEE 754)
- Unexpected program termination (see airplane example)
- Requires defensive programming before every operation
Option B: Return, for example, 0
- Mathematically incorrect
- Masks the error
- Further calculations give false results
Option C: Return null or a special error code
- Requires checking after every operation
- Interrupts the chain of mathematical calculations
- The result type becomes inconsistent
Option D: Special value NaN (chosen by IEEE 754)
- Value propagates through calculations
- Program continues running
- Can check the result at the end
- Maintains type consistency (number)
What Would It Be Like Without NaN?
function divide(a, b) {
// Check types
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Arguments must be numbers');
}
// Check if numbers are valid
if (!isFinite(a) || !isFinite(b)) {
throw new Error('Arguments must be finite');
}
// Check divisor
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
function calculate(expression) {
try {
const result = divide(10, 0);
return result;
} catch (e) {
console.error(e.message);
return null; // What to return? null? undefined? 0?
}
}
What Do We Have Thanks to NaN?
function divide(a, b) {
return a / b; // Hardware does the rest!
}
function calculate(expression) {
return divide(10, 0);
}
const result = calculate("10 / 0");
console.log("Result:", result); // Infinity
const badResult = 0 / 0;
if (Number.isNaN(badResult)) {
console.log("Invalid calculation");
}
Summary
NaN is an elegant solution to the problem of error handling in floating-point calculations:
- Implemented at the hardware level (
ucomisdinstruction) - Part of the IEEE 754 standard since 1985
- Propagates through operations, allowing error detection at the end of calculations
NaN !== NaNis intentional design enabling detectiontypeof NaN === "number"because it’s part of the numeric system, not a separate type
