A Number That Always Returns the Same Thing
Today we’ll look at theNaN type, which in JavaScript is identified as typenumber.
>typeofNaN'number'We get the typenumber 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+1NaN>NaN-1NaN> Math.max(NaN)NaN> Math.min(NaN)NaNAs 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 ofNaN.
// FirefoxboolisNaN()const {return isDouble()&& std::isnan(toDouble()); }// V8if (IsMinusZero(value))return has_minus_zero();if (std::isnan(value))return has_nan();Looking at the code of example browsers, thestd::isnan method from the standard library is used to check forNaN, which may already suggest to us that this is something that appeared independently of JavaScript.
And indeed, looking historically, the first standardization ofNaN appeared in 1985 and was given the numberIEEE 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 howNaN behaves.
>NaN!==NaNtrue>0/0NaN#include<math.h>#include<stdint.h>#include<stdio.h>intmain() {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);return0;}The result is the same as in JavaScript!
NaN is not the samex is NaNNaN hex: 0xfff8000000000000We already know that we encounterNaN in other programming languages as well
#Pythonimport mathnan= float('nan')print(nan!= nan)# Trueprint(nan== nan)# Falseprint(math.isnan(nan))# True//C++#include<iostream>#include<cmath>intmain() {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)return0;}//Rustfnmain() {let nan=f64::NAN;println!("{}", nan!= nan);// trueprintln!("{}", nan== nan);// falseprintln!("{}", 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;# =====================================pxorxmm0,xmm0# xmm0 = 0.0divsdxmm0,xmm0# xmm0 = 0.0 / 0.0 = NaNmovsdQWORDPTR -8[rbp],xmm0# x = NaN# =====================================# if (x != x) {# =====================================movsdxmm0,QWORDPTR -8[rbp]# xmm0 = xucomisdxmm0,QWORDPTR -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)) {# =====================================movsdxmm0,QWORDPTR -8[rbp]# xmm0 = xucomisdxmm0,QWORDPTR -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 thexmm0 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 theucomisd instruction, which is responsible for setting a flag when it detectsNaN.
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>intmain() {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);return0;}The result?
NaN is not the samex is NaNNaN hex: 0xfff8000000000000The program’s output is the same as with high-level C.
We already know thatNaN is natively implemented, so let’s look at theucomisd instruction.
"ucomisdxmm0,%1\n\t"// compare x with x → PF=1 for NaNucomisd - orUnorderedCompareScalarDouble-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 detectNaN using thex != x test in times when theisnan() 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 isintentional 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 dividing0/0.
Operations onnumber 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 withNaN, 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 thatNaN is set at the hardware level, but what was there beforeNaN?
Before the IEEE 754 standard (1985), each hardware manufacturer did it their own way, which usually meant that operations like0/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 anticipate0/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: Returnnull or a special error code
- Requires checking after every operation
- Interrupts the chain of mathematical calculations
- The result type becomes inconsistent
Option D: Special valueNaN (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 WithoutNaN?
functiondivide(a,b) {// Check typesif (typeofa!=='number'||typeofb!=='number') {thrownew Error('Arguments must be numbers'); }// Check if numbers are validif (!isFinite(a)||!isFinite(b)) {thrownew Error('Arguments must be finite'); }// Check divisorif (b===0) {thrownew Error('Division by zero'); }returna/b;}functioncalculate(expression) {try {constresult=divide(10,0);returnresult; }catch (e) {console.error(e.message);returnnull;// What to return? null? undefined? 0? }}What Do We Have Thanks toNaN?
functiondivide(a,b) {returna/b;// Hardware does the rest!}functioncalculate(expression) {returndivide(10,0);}constresult=calculate("10 / 0");console.log("Result:",result);// InfinityconstbadResult=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
