Working with Nullability in JavaScript: Objects and Question Mark Operators
In JavaScript, the values undefined and null — which indicate the absence of a value — are classified as nullish values because of their similarities. That said, there are many notes reminding us not to treat them as the same, such as in their relationship to variable declarations and function parameters.
Nullish values also require special attention in the context of working with objects. Let’s examine this.
Nullish Values in Objects
If we try to access a variable that hasn’t been declared, JavaScript will throw a ReferenceError. But what happens if we try to access a property of an object that hasn’t been declared? JavaScript will return undefined.
console.log(a); // ReferenceError: a is not defined
const b = {};
console.log(b.c); // undefined
Now, suppose we have an object containing product data for a computer component like the one below. The use case is that we want to retrieve its maximum discount amount.
const cpu = {
name: "AMD Ryzen 3700X",
price: {
base: 4950000,
discount: {
rate: 0.1,
maxAmount: 100000,
},
},
};
const discountText = "IDR" + cpu.price.discount.maxAmount; // IDR100000
Now, suppose that data comes from an API, and there’s a convention that products without a maximum discount limit don’t have the price.discount.maxAmount property. What would happen?
const cpu = {
name: "AMD Ryzen 3700X",
price: {
base: 4950000,
discount: {
rate: 0.1,
},
},
};
const discountText = "IDR" + cpu.price.discount.maxAmount; // IDRundefined
Taking it further, what if there’s a convention that products without any discount return null for the price.discount property?
const cpu = {
name: "AMD Ryzen 3700X",
price: {
base: 4950000,
discount: null,
},
};
const discountText = "IDR" + cpu.price.discount.maxAmount;
// Uncaught TypeError: cannot read property 'maxAmount' of null
From the two cases above, it’s clear that accessing nullish properties can become a problem if not handled carefully. The classic way to deal with this is using the && and || logical operators.
Using Logical Operators to Guard Property Access
In JavaScript, the result of a logical operation on non-boolean values returns one of the operands. For example, a && b — if a is a non-boolean truthy value, the operation returns b. If a is falsy, it returns its own value. With the || operator, a || b returns a if a is truthy, or returns b if it is falsy.
Both && and || are short-circuit. This means that in the case of a && b, if a is falsy, it immediately returns a without evaluating b. Similarly, the || operator does the same when its left-hand operand is truthy.
"string" && 42; // 42
undefined && 42; // undefined
null && 42; // null
"string" || 42; // "string"
undefined || 42; // 42
null || 42; // 42
(undefined && false) || 42; // 42
Leveraging these properties of logical operators, we can do this.
const cpu = {
name: "AMD Ryzen 3700X",
price: {
base: 4950000,
discount: null,
},
};
const maxDiscount =
(cpu && cpu.price && cpu.price.discount && cpu.price.discount.maxAmount) ||
"No discount available for this product.";
// "No discount available for this product."
Object property access becomes safer because nullish properties can be handled gracefully. We’ve also added a default value.
But…
This method will fail if the value we expect to be returned is itself falsy. Let’s say the API convention changes again. Now, if there’s no maximum discount, price.discount.maxAmount returns 0. What would happen?
const cpu = {
name: "AMD Ryzen 3700X",
price: {
base: 4950000,
discount: {
rate: 0.1,
maxAmount: 0,
},
},
};
const maxDiscount =
(cpu && cpu.price && cpu.price.discount && cpu.price.discount.maxAmount) ||
"No discount available for this product.";
// "No discount available for this product."
Yet 0 is a perfectly valid value for what we’re searching for and want to evaluate. However, because of the nature of logical operators, the falsy value triggers short-circuiting, which results in the default string “No discount available for this product.” being returned.
There is actually a safer approach — testing each property only for nullish values, like this:
(typeof cpu !== 'undefined' && cpu !== null) &&
(typeof cpu.price !== 'undefined' && cpu.price !== null) && ...
Go ahead and imagine (or try) how long that expression would get. Is there a better way?
Optional Chaining and the Nullish Coalescing Operator
Drawing from the problem of nullish property access in objects, ES2020 introduced two new features: optional chaining and the nullish coalescing operator. Both features make it easier to handle nullish values inside objects, especially when drilling into nested object properties.
Optional Chaining Operator
The optional chaining operator allows developers to access properties deep inside an object without having to validate whether the reference is nullish. Its syntax is a question mark followed by a dot (?.). Let’s try it with our earlier example.
// instead of writing this
typeof cpu !== "undefined" && cpu !== null && cpu.price;
// we can write it more concisely like this!
cpu?.price;
The way this operator works is by returning undefined if the operand on the left side of ? has a nullish value. If the operand on the left has a non-nullish value, the operation proceeds like regular chaining (e.g., a.b). This operator is short-circuiting, meaning it does not evaluate the right-hand operand if the left-hand operand is nullish.
let a = { b: "foo" };
a?.b; // foo
a?.b?.c; // undefined
a?.b?.c?.d; // undefined, because c is undefined
a?.b?.c?.d.e.f.g.h; // undefined, no error because of short-circuit at c
However, keep in mind that a nullish value is different from an undeclared variable. If we try to access a reference that has never been declared — even with the optional chaining operator — we will get a ReferenceError.
foo?.bar; // ReferenceError: foo is not defined
Nullish Coalescing Operator
With the optional chaining operator, we have a shorter and more accurate alternative to checking attribute validity with &&. Now we just need to replace the || part for setting fallback values, which we can do using the nullish coalescing operator.
The syntax of this operator is ??. It works by returning the right-hand operand’s value if the left-hand operand is nullish. Combined with optional chaining, this operator makes attribute access safer and cleaner to read.
let a = { b: "foo" };
a?.b?.c ?? "bar"; // bar
Having met the optional chaining and nullish coalescing operators, let’s compare them against the classic attribute access approach.
(typeof a !== "undefined" &&
a !== null &&
typeof a.b !== "undefined" &&
a.b !== null &&
typeof a.b.c !== "undefined" &&
a.b.c !== null &&
a.b.c) ||
"bar";
a?.b?.c ?? "bar";
Much shorter and clearer, isn’t it?
That covers handling nullish values in the context of objects. When accessing attributes in an object using && and || operators, null and undefined are treated the same as other falsy values, which can lead to false negatives where a valid falsy value is incorrectly treated as invalid. With the optional chaining and nullish coalescing operators introduced in ES2020, accessing potentially-nullish object attributes becomes safer and more concise.
The nullish coalescing operator is essentially designed as a complementary syntax to the optional chaining operator. However, optional chaining itself has more use cases than those covered in this post. A follow-up article diving deeper into them is coming. Stay tuned!
References and Further Reading
- Optional Chaining (?.) | MDN
- Nullish Coalescing Operator (??) | MDN
- tc39 Proposal for Optional Chaining | GitHub - Contains details on motivation and advanced syntax
- tc39 Proposal for Nullish Coalescing | GitHub - Contains details on motivation and advanced syntax