undefined
and null
Many programming languages have one “non-value” called null
. It indicates that a variable does not currently point to an object – for example, when it hasn’t been initialized yet.
In contrast, JavaScript has two of them: undefined
and null
.
undefined
vs. null
Both values are very similar and often used interchangeably. How they differ is therefore subtle. The language itself makes the following distinction:
undefined
means “not initialized” (e.g., a variable) or “not existing” (e.g., a property of an object).
null
means “the intentional absence of any object value” (a quote from the language specification).
Programmers sometimes make the following distinction:
undefined
is the non-value used by the language (when something is uninitialized, etc.).
null
means “explicitly switched off”. That is, it helps implement a type that comprises both meaningful values and a meta-value that stands for “no meaningful value”. Such a type is called option type or maybe type in functional programming.
undefined
and null
The following subsections describe where undefined
and null
appear in the language. We’ll encounter several mechanisms that are explained in more detail later in this book.
undefined
Uninitialized variable myVar
:
let myVar;
assert.equal(myVar, undefined);
Parameter x
is not provided:
function func(x) {
return x;
}
assert.equal(func(), undefined);
Property .unknownProp
is missing:
const obj = {};
assert.equal(obj.unknownProp, undefined);
If we don’t explicitly specify the result of a function via a return
statement, JavaScript returns undefined
for us:
function func() {}
assert.equal(func(), undefined);
null
The prototype of an object is either an object or, at the end of a chain of prototypes, null
. Object.prototype
does not have a prototype:
> Object.getPrototypeOf(Object.prototype)
null
If we match a regular expression (such as /a/
) against a string (such as 'x'
), we either get an object with matching data (if matching was successful) or null
(if matching failed):
> /a/.exec('x')
null
The JSON data format does not support undefined
, only null
:
> JSON.stringify({a: undefined, b: null})
'{"b":null}'
undefined
or null
Checking for either:
if (x === null) ···
if (x === undefined) ···
Does x
have a value?
if (x !== undefined && x !== null) {
// ···
}
if (x) { // truthy?
// x is neither: undefined, null, false, 0, NaN, 0n, ''
}
Is x
either undefined
or null
?
if (x === undefined || x === null) {
// ···
}
if (!x) { // falsy?
// x is: undefined, null, false, 0, NaN, 0n, ''
}
Truthy means “is true
if coerced to boolean”. Falsy means “is false
if coerced to boolean”. Both concepts are explained properly in “Falsy and truthy values” (§17.2).
??
) for default values ES2020
The nullish coalescing operator (??
) lets us use a default if a value is undefined
or null
:
value ?? defaultValue
value
is undefined
nor null
, defaultValue
is evaluated and the result is returned.
value
is returned.
Examples:
> undefined ?? 'default'
'default'
> null ?? 'default'
'default'
> false ?? 'default'
false
> 0 ?? 'default'
0
> '' ?? 'default'
''
> {} ?? 'default'
{}
??
is short-circuiting??
is short-circuiting – the right-hand side is only evaluated if it is actually used:
let evaluated = false;
// Right-hand side is not used
123 ?? (evaluated = true);
assert.equal(evaluated, false);
// Right-hand side is used
undefined ?? (evaluated = true);
assert.equal(evaluated, true);
The following code shows a real-world example:
function countMatches(regex, str) {
const matchResult = str.match(regex); // null or Array
return (matchResult ?? []).length;
}
assert.equal(
countMatches(/a/g, 'ababa'), 3
);
assert.equal(
countMatches(/b/g, 'ababa'), 2
);
assert.equal(
countMatches(/x/g, 'ababa'), 0
);
If there are one or more matches for regex
inside str
, then .match()
returns an Array. If there are no matches, it unfortunately returns null
(and not the empty Array). We fix that via the ??
operator.
We also could have used optional chaining:
return matchResult?.length ?? 0;
function getTitle(fileDesc) {
return fileDesc.title ?? '(Untitled)';
}
const files = [
{ path: 'index.html', title: 'Home' },
{ path: 'tmp.html' },
];
assert.deepEqual(
files.map(f => getTitle(f)),
['Home', '(Untitled)']
);
||
) for default valuesBefore ECMAScript 2020 and the nullish coalescing operator, logical Or was used for default values. That has a downside.
||
works as expected for undefined
and null
:
> undefined || 'default'
'default'
> null || 'default'
'default'
But it also returns the default for all other falsy values – for example:
> false || 'default'
'default'
> 0 || 'default'
'default'
> 0n || 'default'
'default'
> '' || 'default'
'default'
Compare that to how ??
works:
> undefined ?? 'default'
'default'
> null ?? 'default'
'default'
> false ?? 'default'
false
> 0 ?? 'default'
0
> 0n ?? 'default'
0n
> '' ?? 'default'
''
??=
) ES2021
The nullish coalescing assignment operator (??=
) assigns a default if a value is undefined
or null
:
value ??= defaultValue
value
is either undefined
or null
, defaultValue
is evaluated and assigned to value
.
Examples:
let value;
value = undefined;
value ??= 'DEFAULT';
assert.equal(
value, 'DEFAULT'
);
value = 0;
value ??= 'DEFAULT';
assert.equal(
value, 0
);
??=
is short-circuitingThe following two expressions are roughly equivalent:
a ??= b
a ?? (a = b)
That means that ??=
is short-circuiting – the following two things only happen if a
is undefined
or null
:
b
is evaluated.
a
.
let value;
value = undefined;
value ??= console.log('evaluated');
value = 0;
value ??= console.log('NOT EVALUATED');
evaluated
??=
to add missing properties
const books = [
{
isbn: '123',
},
{
title: 'ECMAScript Language Specification',
isbn: '456',
},
];
// Add property .title where it’s missing
for (const book of books) {
book.title ??= '(Untitled)';
}
assert.deepEqual(
books,
[
{
isbn: '123',
title: '(Untitled)',
},
{
title: 'ECMAScript Language Specification',
isbn: '456',
},
]);
undefined
and null
don’t have propertiesundefined
and null
are the only two JavaScript values where we get an exception if we try to read a property. To explore this phenomenon, let’s use the following function, which reads (“gets”) property .prop
and returns the result.
function getProp(x) {
return x.prop;
}
If we apply getProp()
to various values, we can see that it only fails for undefined
and null
:
> getProp(undefined)
TypeError: Cannot read properties of undefined (reading 'prop')
> getProp(null)
TypeError: Cannot read properties of null (reading 'prop')
> getProp(true)
undefined
> getProp({})
undefined
undefined
and null
In Java (which inspired many aspects of JavaScript), initialization values depend on the static type of a variable:
null
.
int
variables are initialized with 0
.
JavaScript borrowed null
and uses it where objects are expected. It means “not an object”.
However, storage locations in JavaScript (variables, properties, etc.) can hold either primitive values or objects. They need an initialization value that means “neither an object nor a primitive value”. That’s why undefined
was introduced.