On all the Question Marks in JS
The ECMAScript 2021 Language Specification includes two new syntax features “to improve working with ‘nullish’ values (null
or undefined
): nullish coalescing, a value selection operator; and optional chaining, a property access and function invocation operator”1. With these two additions we will see way more question marks popping up in our JS code bases. In this article I will present all three features that use question marks in JS (nullish coalescing, optional chaining, ternary operator) and their use cases.
Nullish Coalescing
The nullish coalescing operator serves to assign an alternative value in case the given value is null
or undefined
. It works like this:
const notNull = null ?? 'Yay! Rescued from null!'
console.log(notNull)
// -> Yay! Rescued from null!
Other than the logical OR operator, the nullish coalescing operator doesn’t affect other values that are regarded as falsy which makes it handy if we want to reassign undefined
and null
but not ''
and 0
.
Imagine we have some number input on a website that should default to 1
when not set, but the user has the option to set 0
as a value. We would like to just write this:
function processNumberInput(value) {
value = value || 1
return value.toString()
}
The problem with this implementation is that in this case we will reassign value
when it’s 0
. We are enraged: “God damn it, JS is so annoying.” But then we remember that there are default Parameters and that JS isn’t so bad after all…
function processNumberInput(value = 1) {
return value.toString()
}
The next day we see a new bug ticket on our company’s bug board saying: TypeError: Cannot read property 'toString' of null at processNumberInput
. Optional parameters only reassign undefined
arguments but not null
. Only god knows why someone would pass null
to our function but it is our job to make sure that we take care of such a case. Rightfully, we wonder why Branden Eich needed to implement JS with several types to express the absence of value! But I guess when you need to come up with a new language in 10 days you have no time to think of such details.
Because of Branden’s decision we have no other option than to rewrite our function like this:
function processNumberInput(value) {
if (!value && value !== 0) value = 1
return value.toString()
}
This is not comfortable to write. To constantly think of handling null
as well as undefined
while we are supposed to produce productive code is an unnecessary cognitive burden. The nullish coalescing operator changes this for us and makes it easy to deal with the absence of value. Now we can just write the following and we will be covered.
function processNumberInput(value) {
value = value ?? 1
return value.toString()
}
Optional Chaining
Optional chaining deals with nullish values as well and just like the nullish coalescing operator it makes our life easier when we need to deal with a possible absence of value. Optional chaining allows us to attempt to access an object’s properties without risking to cause an error. It works with any kind of property including methods. If the property doesn’t exist it will be evaluated as undefined
instead of throwing something like TypeError: Cannot read property 'foo' of bar
.
On the proposal’s Github repository you can find this syntax summary2:
obj?.prop // optional static property access
obj?.[expr] // optional dynamic property access
func?.(...args) // optional function or method call
But where would we use optional chaining? Let’s go through an example. Let’s imagine we want to access the location
property or a user
object that we just fetched from an API. Before optional chaining if we tried to access an object’s property and we couldn’t be certain it existed we would have needed to wrap it in long conditionals like this one:
function getHomeLocation(user) {
if (user && user.home && user.home.location) {
return person.home.location
}
return 'No home location entered'
}
Optional chaining enables us to express the conditional statement in a more concise way and make the overall construct more readable.
function getHomeLocation(user) {
if (user?.home?.location) {
return user.home.location
}
return 'No home location entered'
}
Instead of explicitly checking each property of our call, we write the call with the optional chaining operators in between.
Since the attempted access of location won’t throw an error anymore with optional chaining but return undefined
, we can leave the conditional away and formulate the function even more concise.
function getHomeLocation(user) {
return user?.home?.location || 'No home location entered'
}
Optional chaining will change a lot for devs working with APIs that return variable JSON output or with user generated input.
It is important to note that once used inside an object call you need to continue to use optional chaining because otherwise you will get a TypeError: Cannot read property 'foobar' of undefined
.
user.home?.address.street // will cause `TypeError: Cannot read property 'street' of undefined`
user.home?.address?.street // won't cause an error
Also, you can’t use optional chaining for assignments.
function setHomeLocation(user) {
user?.home?.location = 'Berlin'
}
This assignment would cause an Error: Invalid left-hand side in assignment
because the left-hand side could be evaluated as undefined
.
Ternary Operator
The conditional operator which is more widely known as the ternary operator was already part of JS before the ECMAScript standard started to expand the language. Before the ECMAScript 2021 Specification this was the only place you would find a question mark outside of a string in JS code.
Basically, the ternary operator is a shorthand for a classical if/else
statement. For example consider a simple control flow with an if/else
statement like the following:
function getBiggest(first, second) {
if (first >= second) {
return first
} else {
return second
}
}
The same function could be written this way with a ternary operator
function getBiggest(first, second) {
return first >= second ? first : second
}
The ternary operator takes a condition before the ?
and when the expression is true
it executes the part before the :
otherwise it executes the part after it. As you can see it allows us to write simple control flows in a more concise way without losing any of the original readability.
As soon as we write more complex control flows including an else if
for example the ternary operator should be avoided most of times because we would need to nest multiple ternary operators inside of each other which makes you very unpopular with anyone who needs to read your code including your future self. Please never use the ternary operator to write something like this:
function getBiggest(one, two, three) {
return (one >= two && one >= three)
? one
: (two >= one && two >= three)
? two
: three
}
No Question Marks Left
There you have it. All there is to know about JS’ question marks. I hope it will help you to write safer, more concise code that is less bug prone!
References
- Refactoring optional chaining into a large codebase: lessons learned
- Nullish Coalescing for JavaScript (Github)
- Optional Chaining for JavaScript (Github)
- Harband J., Guo S., Ficarra M. and Gibbons K. (2020), “Introduction”, ECMAScript 2021 Language Specification.↩
- tc39 (2020), “Syntax”, proposal-optional-chaining (Github).↩