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 (always false when 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
  • NaN propagation (any operation with NaNNaN)

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.0NaN
  • ∞ - ∞NaN
  • 0 * ∞NaN
  • sqrt(-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 (ucomisd instruction)
  • Part of the IEEE 754 standard since 1985
  • Propagates through operations, allowing error detection at the end of calculations
  • NaN !== NaN is intentional design enabling detection
  • typeof NaN === "number" because it’s part of the numeric system, not a separate type

References