The many faces of undefined in JavaScript
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 typeundefined
as missing: A generated value when a field isn’t knownundefined
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 beenundefined
. But I also created a more complexMissingSentryType
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 toif( 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 writeif( 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 ifsomeObject
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.