July 23, 2022 7 min read

Is it native JavaScript? Isn't it??

My first encounter with optional chaining and nullish coalescing operator.

Last updated: July 23, 2022
Question mark composed with dots on yellow background
Photo by Anna Shvets

The first time I used Gatsby, I spotted some strange code snippet.

src/components/seo.jsJSX
1const defaultTitle = site.siteMetadata?.title

I wondered, "what's with that question mark after dot? Is it some special Gatsby syntax?" That didn't make sense to me, so I started looking for more information. After some research and Googling stupid questions, I found the term I was looking for - optional chaining. I was surprised - it is a native JavaScript. This syntax was introduced in ES2020 (on the day of writing this post, it is at stage four of the proposal process). First, we look at some errors to see why it's useful.

39nth Technical Committee (TC39) is a group under ECMA International that contain JavaScript developers, implementers, and academics. The committee collaborates with the community to maintain and evolve ECMAScript specification (JavaScript conforms to that specification). The development process has four stages. The fourth stage means the feature is ready to be included in the last draft of the specification.

If you're not a complete JavaScript beginner, you've probably seen a message like this: "Cannot read properties of undefined." It usually means that you want to access a property of a nested object that doesn't exist. Let's reproduce this error, but this time on purpose. Imagine you want to display detailed information about computers - laptops and desktops. They almost consist of the same parts, but some laptops don't have a dedicated graphics card - there is no information about it. If you try to access that information, you'll get our error.

JS
1const desktop = {
2 processor: {
3 manufacturer: 'Intel',
4 type: 'I7'
5 },
6 graphicsCard: {
7 manufacturer: 'Nvidia'
8 }
9 //...more components
10}
11
12const laptop = {
13 processor: {
14 manufacturer: 'AMD',
15 type: 'Ryzen 5'
16 }
17 //...more components without graphics card
18}
19
20const info = laptop.graphicsCard.manufacturer
21//Uncaught TypeError: Cannot read properties of undefined (reading 'manufacturer')

Let's try to solve that error. First of all, we can fill in that information.

JS
1const laptop = {
2 processor: {
3 manufacturer: 'AMD',
4 type: 'Ryzen 5'
5 },
6 graphicsCard: {
7 manufacturer: ''
8 }
9}
10
11const info = laptop.graphicsCard.manufacturer //No error. Returns ""

But what, when we couldn't? We don't always have control over API. Another problem is when an object has more properties - every one of them should be an empty string, null, etc. Not ideal. JavaScript is a dynamic programming language, so there can be null or undefined values. We need to deal with situations like this. We can use a logical expression to solve this.

JS
1const laptop = {
2 processor: {
3 manufacturer: 'AMD',
4 type: 'Ryzen 5'
5 }
6}
7
8const info = laptop && laptop.graphicsCard && laptop.graphicsCard.manufacturer //undefined

It works. But it is verbose and clunky. We're only accessing one property, and we need two logical operators. Imagine there were many of them. Maybe conditionals will help.

JS
1const laptop = {
2 processor: {
3 manufacturer: 'AMD',
4 type: 'Ryzen 5'
5 }
6}
7
8const info = laptop
9 ? undefined
10 : laptop.graphicsCard
11 ? undefined
12 : laptop.graphicsCard.manufacturer //undefined

Was I writing something about clunky code? That looks even worse. Nested ternary operators are rarely a good idea. But JavaScript has syntax for error handling. Let's try...to catch this error.

JS
1const laptop = {
2 processor: {
3 manufacturer: 'AMD',
4 type: 'Ryzen 5'
5 }
6}
7
8let info
9try {
10 info = laptop.graphicsCard.manufacturer
11} catch (error) {
12 info = undefined //undefined
13}

It is more readable, but it is still verbose. We're defining new scopes between braces and we can't use const. It's time to solve this problem like a real, JavaScript developer - let's use a third-party library!

JS
1const R = require('ramda')
2
3const laptop = {
4 processor: {
5 manufacturer: 'AMD',
6 type: 'Ryzen 5'
7 }
8}
9
10const info = R.path(['graphicsCard', 'manufacturer'], laptop) //undefined

This snippet is concise and readable. But do we need to use a third-party library for something basic like this? No, not anymore. Optional chaining comes to the rescue!

JS
1const laptop = {
2 processor: {
3 manufacturer: 'AMD',
4 type: 'Ryzen 5'
5 }
6}
7
8const info = laptop?.graphicsCard?.manufacturer //undefined

With optional chaining, you can try to access nested properties that may not be available. The above snippet won't throw an error. It will return undefined. If a reference is null or undefined, the expression returns undefined. In my opinion, the ?. operator is concise and intuitive. Is like a question: "Is there a manufacturer (of the graphics card) property on the laptop object?". If so - return it. Otherwise, return undefined. This operator is more powerful. You can also use the syntax with function calls.

JS
1const laptop = {
2 processor: {
3 manufacturer: 'AMD',
4 type: 'Ryzen 5'
5 }
6}
7
8const info = laptop.nonExistingMethod?.() //undefined

The object has not any method. Still, the snippet above doesn't throw an exception. It returns undefined. Let's go further - can you use this operator with arrays? Yes, you can.

JS
1const laptop = {
2 ram: ['Kingston 8GB', 'Kingston 8GB']
3}
4
5const info = laptop.ram?.[3] //undefined

Even though there are no four elements in the array, the code doesn't throw an error.

Nullish coalescing operator

If we "add" another question mark to our operator and remove the dot, we get a new logical operator - the nullish coalescing operator.

JS
1const laptop = {
2 processor: {
3 type: 'Ryzen 5'
4 }
5}
6
7const info = laptop.graphicsCard?.manufacturer ?? 'integrated' //integrated

You can interpret the above snippet like this: "If a graphics card manufacturer in the laptop object exists, assign its value to info variable. Otherwise, assign the integrated string." From the previous section, we know that our optional chaining returns undefined. Then interpreter moves to the logical operator. The nullish coalescing operator is a specific case of logical OR. It returns the right-hand side operand when its left-hand side operand is nullish (null or undefined). Logical OR returns the right-hand side operand if the left-hand side operand is any falsy value.

JS
1const a = null ?? 'default' //"default"
2const b = undefined ?? 'default' //"default"
3const c = '' ?? 'default' //""
4const d = NaN ?? 'default' //NaN - it returns every other falsy value
5
6const e = '' || 'default' //"default"
7const f = NaN || 'default' //"default"
8const g = 0 || 'default' //"default" - it returns "default" for falsy values

The behavior of logical OR can lead to unexpected errors. For example, if you want a default number, but 0 is a correct, expected value. That's why the nullish coalescing operator is handy. It is stricter and can prevent bugs like this. But it does not replace logical OR. Also, you can't chain the nullish coalescing operator with other logical operators without parenthesis.

null || undefined ?? "default"

The above code raises a syntax error.

(null || undefined) ?? "default"

The above code is correct and returns "default.”

Bonus - logical nullish assignment

While researching this post, I found one more operator with a double question mark. If we "add" an equal sign to the nullish coalescing operator, we get the logical nullish assignment. It is easy to deduce what it is doing. Instead of returning, it assigns a particular value to x if x is nullish.

JS
1const laptop = {
2 processor: 'Intel'
3}
4
5laptop.processor ??= 'AMD'
6laptop.graphicsCard ??= 'Nvidia'
7
8console.log(laptop.processor) //"Intel"
9console.log(laptop.graphicsCard) //"Nvidia"

It is a shortcut for the expression with the nullish coalescing operator. The two expressions below are equivalents.

JS
1const laptop = {
2 processor: 'Intel'
3}
4
5laptop.graphicsCard ??= 'Nvidia'
6laptop.graphicsCard ?? (laptop.graphicsCard = 'Nvidia')
7//Above two expressions do the same thing
8
9console.log(laptop.graphicsCard) //"Nvidia"

If you want more examples or details, visit MDN Web Docs:

A newsletter that sparks curiosity💡

Subscribe to my newsletter and get a monthly dose of:

  • Front-end, web development, and design news, examples, inspiration
  • Science theories and skepticism
  • My favorite resources, ideas, tools, and other interesting links
I am not a Nigerian prince to offer you opportunities. I do not send spam. Unsubscribe anytime.

Stay curious. Read more

Half of a record on white backgroundSeptember 1, 20227 min read

Accessible animations in React

Or how not to spin your users round (like a record). Some animations can make users sick. We'll take care of them and make non-essential animations optional.

Read post
List of CSS variables in Visual Studio Code.September 14, 20228 min read

Converting design tokens to CSS variables with Node.js

Converting design tokens is an error-prone process - I found about it the hard way. So, I made a simple Node.js script that will help me with that task.

Read post
Five metal gears on a black brackgroundSeptember 23, 202211 min read

Gatsby with Netlify CMS

In this post, we will look closely at a Netlify CMS. It is an example of a new type of CMS that is git-based. We will integrate it with a Gatsby example project.

Read post