The many faces of undefined in JavaScript

Edaqa Mortoray
Uncountable Engineering
9 min readJun 26, 2024

Why does JavaScript have so many ways to represent unknown values? Of the ones I’ve found, only a couple seem necessary. The rest of these unknowns create confusion and lead to defects in code. It’s an unfortunate situation, so one worth understanding if you code in JavaScript.

Below, I summarize how we can represent missing data, unknowns, or undefined values in JavaScript. In this article, I’ll give more details to each of them.

The Simple Cases

  • null: A real value with a distinct type
  • undefined as missing: A generated value when a field isn’t known
  • undefined as value: A real value with a distinct type assigned to a variable

The Interpretative Cases

  • Falsy: When a real value looks undefined
  • ReferenceError: When a symbol isn’t known
  • <empty slot>: In sparse arrays
  • -1 as undefined: Returned from some functions
  • undefined replacement: A confusing situation

The TypeScript Cases

  • function arguments: unspecified arguments and explicit undefined
  • void: a special TypeScript version

The Simple Cases

null

Null is the classic null value. It is generally used to mean that a value is optional, but has not been provided. I assume many coders would be happy if this were the only undefined value in a language. Indeed, many other languages get away with just one. But not JavaScript; we’re just getting started.

undefined as missing

The concept of undefined initially seems to make sense. Whereas null specifies an actual value, an undefined would indicate the value doesn’t actually exist. This makes sense if you consider this structure, using TypeScript notation:

interface Arguments {
nullable_value: number | null
optional_value?: number | null
}

We’re saying that nullable_value is a required property of this structure: you can’t omit it. It can, however, be a null value. optional_value is also nullable, but it doesn’t need to exist at all. undefined lets us distinguish between a specified null value, and the value not existing at all.

const a = {
nullable_value: null,
optional_value: null,
}

const b = {
nullable_value: null,
}

With these variables, a.optional_value === null. In b however, b.optional_value !== null but b.optional_value === undefined, since optional_value isn’t defined in b at all. On casual use, this feels like a decent way to report, or detect, values that are missing from a structure.

Alas, lossy comparison has made this confusing. I used === null to check strictly for the null value. The expression b.optional_value == null is true, since an undefined value loosely compares as equal to null. Since equality is the same with the arguments swapped, that means that b.nullable_value == undefined is also true.

Most other languages don’t have this concept of undefined. In the C++ space, undefined isn’t a specific value, but means precisely the value is undefined: it does not have a determinate state and could be anything at all.

undefined as a value

The comparison to undefined requires that undefined is a symbol in the language. And for consistency of syntax, it is a real value in the language. This is a choice JavaScript made, likely for implementation simplicity, instead of keeping undefined a special symbol.

Here’s where the first bit of oddity shows up, in case undefined itself wasn’t already odd enough for you. What if I specify this constant:

const c = {
nullable_value: null,
optional_value: undefined,
}

I explicitly said optional_value is undefined. So does this structure actually define the optional_value property?

If I serialize to JSON and back, using the standard JSON module, the explicit undefined will be stripped, like we didn’t specify it at all. That’s perhaps good. Additionally, the expression c.optional_value === undefined is true. That is good. It seems like it’s equivalent to not specifying the value at all…

…but wait? Object.hasOwn( c, ‘optional_value’ ) is true. So c does have the optional_value property. Indeed, if I emit c on the console, it will print out optional_value: undefined. This is in contrast to the object b I defined before, where Object.hasOwn( b, ‘optional_value’ ) is false.

A Confusing Convention

This establishes that undefined as a property value is distinct from a property not existing. This inconsistency is a common source of undefined-value-related defects in code.

This also undermines, and confuses, the purpose of undefined. It does not mean a value does not exist, but is rather just an alternate form of null. It’s only by convention that we consider undefined values to be equivalent to missing values. And not all libraries uphold this convention, not even the standard JavaScript library.

This convention caused problems when I wrote a cross-language serialization format. To ease the Python side of things, I ended up treated any types as null | undefined in JavaScript that could have just been undefined. But I also created a more complex MissingSentryType to match a true unknown.

The Interpretative Cases

Now, let’s look at some of the more esoteric undefined values. These are language semantics that clearly introduce undefined values, but in indirect ways.

falsy

We have to consider falsy values in this discussion. A lot of code checks for null in conditional statements like below:

if( someObject ) {
someObject.call()
}

This if-expression is false someObject is null or undefined, which is what was desired here. It’s also false for a boolean false. Additionally, it’s false for the value 0 and an empty string; I’ve seen these two cause defects in code that thinks it’s checking for null.

We find this falsy pattern in other languages, like C. However, in C it cannot lead to the same defects as in JavaScript. Pointers are distinct types and require explicit dereferencing. You can’t accidentally interpret a zero integer as an null pointer. If you had an int* someObject in C, the above JavaScript if-statement would be equivalent to if( someObject && *someObject ).

ReferenceError

If I type an unknown symbol into the browser console, I get a ReferenceError exception.

> nothing_here

Uncaught ReferenceError: nothing_here is not defined

Yet if I type window.nothing_here I get an undefined value. window is supposedly a view on the global data (or `global` in NodeJS). Why does nothing_here have a different way of reporting it’s missing depending on how I access it?

This inconsistency also happens with functions.

function missing() {
console.log( this.nothing_here )
console.log( nothing_here )
}

Calling this function emits undefined followed by a ReferenceError. It’s easy to understand the difference that leads to the inconsistency. It’s, however, hard to understand why we’d want this inconsistency.

<empty slot>

Arrays in JavaScript can be sparse, which introduces yet another way to create missing values.

arr1 = []
arr1[0] = "Zero"
arr1[2] = "Two"

If I emit arr1 to the console, I get Array(3) [ “Zero”, <1 empty slot>, “Two” ] as the short output, at least in Firefox. Except, if I expand the console output, I get the below.

0: "Zero"
2: "Two"
length: 3

It shows only two elements, which makes sense, but the length is still 3. Accessing arr[1] results in undefined.

But what if I create an array with an explicit undefined value?

arr2 = ["Zero", undefined, "Two"]

Emitting arr2 to the console produces Array(3) [ “Zero”, undefined, “Two” ]. So an empty slot is not the same as an undefined value in the array, even if they have the same length and the value at index 1 is undefined in both cases. You can tell they are different if you call .flat() on them. arr1.flat() == [“Zero”, “Two”] whereas arr2.flat() == [“Zero”, undefined, “Two”]. The core language seems to ignore the convention about treating undefined as a missing value.

If you do wish to get an empty slot in an existing array, you can use the delete operator. delete arr2[1] removes the undefined value, replacing it with the empty slot.

-1 as missing value

Despite having both undefined and null values, Array.indexOf returns -1 if it can’t find an element in an array. Though Array.find is happy to return undefined when it can’t find an element.

Arrays aren’t strictly arrays in JavaScript, though. They are objects which can behave somewhat like an array. They can also be sparse, as we saw previously. These leads to a bit of fun with the indexOf function.

arr = ["Zero", "One"]
arr[-1] = "Minus"
arr.indexOf("Minus") // returns -1

indexOf here returns -1, not because it found it at that index, but because it only checked the proper indexes and didn’t find it. This can lead to odd defects if you forget to check the return value.

const someArray = [];
const at = someArray.indexOf("Source");
someArray[at] = "Target";
someArray[someArray.indexOf("Target")] == "Target"

I’ve permanently stuck something at the “not found” index, such that if I look for it, it’ll be not found, yet also found just the same.

undefined replacement

undefined isn’t a reserved symbol in JavaScript, at least not in a function. This lets us create a custom value for undefined.

undefined = 123
console.log(undefined) // prints "undefined"

function stupidCode() {
const undefined = 123
console.log( undefined ) // prints "123"
}

Oddly, if I attempt to write const undefined = 123 in the global scope, I get a syntax error, though undefined = 123 is fine, but does nothing.

The TypeScript Cases

function arguments

TypeScript adds an extra twist with optional arguments in function signatures. Consider this function:

function missingValue(a: number, b?: number) {
console.log(arguments)
console.log(a, b)
}

missingValue(1)
missingValue(2, undefined)

b? is an optional argument. In the body of the function, it has the type number | undefined, but that’s not strictly correct. It has either the type number or it has a missing value which evaluates to undefined. We can see this when we print out the arguments value. In the first call, it has only one element, and in the second it has two elements, with an explicit undefined in the second position.

Thus, this function works differently when called as missingValue(1) versus missingValue(1, undefined).

void

TypeScript introduces one additional form of unknown: void. You probably recognize this most when declaring a function that doesn’t define a return value.

function noReturn() {
}

type CallWithoutReturn = () => void
const proc : CallWithoutReturn = noReturn

However, void is just another type in TypeScript. If you have strict null checks enabled, it’s equivalent to undefined, otherwise it’s equivalent to undefined | null. That is, it seems to serve no purpose at all. Consider this code, in which all statements and expressions are valid.

function noReturn() {
return undefined
}

function undefinedReturn() : undefined {
// no explicit return
}

function impliedMismatch() : void {
const a = undefined
return a
}

const a : void = undefined
const b: void = noReturn()
const c : undefined = noReturn()
const d : void = undefinedReturn()

I could have replaced all void with undefined. void doesn’t introduce any new semantics to the language.

Undefined Conclusion

In short, the meaning of undefined is “undefined”.

While this may not matter in many places, it has adverse effects in a few common use-cases: serialization and dictionaries. For serialization, one has to decide whether all missing values are the same, or explicit undefined’s are treated specially, or even distinct from null for that matter. When using a JavaScript object as a dictionary, which we all so often do, also has pitfalls. Iteration differs between missing values and explicit undefined values. Calls to introspection functions, like hasProperty, also have to be used carefully.

It’s likely that these inconsistencies arose due to the evolution of the language, trying to account for the oddity of the original unspecified JavaScript. TypeScript also kept this all as-is, thus failing to add significant safety to the most commonly used values in code. It’s unfortunate, since it means people introduced to coding through the web are exposed to the most confused system of missing values of any common programming language.

Recommendations

But in the end, you have to use this language and you can’t avoid undefined values. Here are some things I’ve found that can limit the exposure to the defects created by the many faces of undefined.

  • Don’t use falsy comparisons: Always use an explicit comparison operator, especially when checking for null. This will avoid ever having an unintended equality. That is, don’t write if( someObject ), instead write if( someObject != null)
  • Defensively treat undefined as null: As much as possible, assume that an optional value can be undefined or null. Rely on undefined loosely comparing to null, for comparisons such as someObject == null, which is true if someObject is null or undefined.
  • Avoid distinguishing null and undefined: Avoid creating code that produces distinct null and undefined values. If a function has multiple undefined results, instead return an object with explicit flags in it.
  • Distrust convention: In places where you truly need a distinct undefined and null, such as serialization, you need to be rigorous in analysing code. Assume that all libraries, including JavaScript’s own, don’t do what you need. Test each called function and be strict on the TypeScript typing.
  • Don’t use clever tricks: Do not exploit any of the weird things I’ve shown. Do not rely on any of the inconsistencies.

--

--

Edaqa Mortoray
Uncountable Engineering

Stars and salad make me happy. I‘ve been writing, coding, and thinking too much for over 20 years now.