Optional Chaining and Nullish Coalescing in Node.js
Optional chaining and nullish coalescing are now available in Node.js 14, making it easier to write safe, concise code without defensive programming.
Optional chaining and nullish coalescing are now available in Node.js 14, making it easier to write safe, concise code without defensive programming.
I've been eagerly waiting for this moment! Optional Chaining and Nullish Coalescing have been absolute game changers in my TypeScript projects since November 2019, and now that ECMAScript 2020 officially landed in June, these features are finally available everywhere that matters.
Node.js 14 shipping with native support means I can now use these operators across my entire stack without any transpilation overhead. As someone who spends most of their time building applications in TypeScript and Node.js, watching these operators go from TypeScript-only features to universal JavaScript has been incredibly exciting.
The developer response has been overwhelmingly positive. In fact, optional chaining became one of the most requested TypeScript features long before it hit the official spec. Now that both features are officially part of the language and natively supported in Node.js 14, it's time to dive deep into how they work and why they matter.
Let me paint a picture that probably sounds familiar. You're building an API endpoint in Express.js, handling user data that comes in various shapes:
// The old way - defensive programming nightmare
app.post("/jedi", (req, res) => {
let comlink;
if (req.body && req.body.contact && req.body.contact.comlink) {
comlink = req.body.contact.comlink;
} else {
comlink = "no-comlink@rebelalliance.org";
}
let lightSaberColor;
if (req.body && req.body.preferences) {
lightSaberColor = req.body.preferences.lightSaberColor || "blue"; // But wait - what if lightSaberColor is ""?
} else {
lightSaberColor = "blue";
}
// ...and so on
});
This kind of defensive programming clutters codebases and makes simple operations verbose. I've written this pattern hundreds of times, and I bet you have too. Optional chaining and nullish coalescing eliminate this boilerplate entirely.
Optional chaining allows you to safely access nested object properties without explicitly checking if each reference in the chain is valid. The magic happens with short-circuiting: if any part of the chain is null or undefined, the entire expression returns undefined instead of throwing a TypeError.
The syntax supports three forms:
obj?.propobj?.[expression]func?.(args)Here's how I've been using optional chaining in my recent projects:
// Express.js route - much cleaner!
app.get("/jedi/:id", async (req, res) => {
try {
const jedi = await Jedi.findById(req.params.id);
const response = {
name: jedi?.name ?? "Unknown Jedi",
comlink: jedi?.contact?.comlink ?? "No comlink provided",
homeworld: jedi?.origin?.homeworld ?? "Planet not specified",
isActive: jedi?.status?.isActive ?? true,
};
res.json(response);
} catch (error) {
res.status(500).json({
error: error?.message ?? "A disturbance in the Force occurred",
});
}
});
Dynamic method invocation becomes elegant too:
// Death Star API client with optional method calling
class DeathStarAPIClient {
async callMethod(system, method, ...args) {
const handler = this.systems?.[system]?.[method];
return (
(await handler?.(...args)) ?? Promise.resolve("System not operational")
);
}
}
This is where things get exciting. Node.js 14.0.0 (released in April 2020) shipped with full native support - no flags required! This was a big moment because it meant we could use these features in production Node.js applications immediately.
The support timeline has been impressive:
For TypeScript projects (which I use extensively), I've been enjoying these features since TypeScript 3.7 dropped in November 2019. Being able to use optional chaining and nullish coalescing months before they were officially standardized felt like having a glimpse into the future of JavaScript.
Pros I've experienced:
Cons to watch out for:
undefinedI've found the sweet spot is using optional chaining for external data (API responses, user input, configuration) while being more explicit with internal application state.
Here's what makes nullish coalescing special: it only treats null and undefined as falsy. The logical OR operator (||) treats many valid values as falsy, which often isn't what we want.
This distinction is crucial for configuration and user preferences:
// The problem with || operator
function startDeathStar(options = {}) {
const powerLevel = options.powerLevel || 100; // What if powerLevel is 0?
const shieldsUp = options.shieldsUp || 3; // What if shieldsUp is 0?
const stealthMode = options.stealthMode || false; // What if stealthMode is false?
}
// The solution with ?? operator
function startDeathStar(options = {}) {
const powerLevel = options.powerLevel ?? 100; // 0 is preserved ✅
const shieldsUp = options.shieldsUp ?? 3; // 0 is preserved ✅
const stealthMode = options.stealthMode ?? false; // false is preserved ✅
}
I've been using nullish coalescing extensively in configuration management:
// Imperial fleet configuration
const config = {
starDestroyerCount: process.env.STAR_DESTROYER_COUNT ?? 25,
commandShip: process.env.COMMAND_SHIP ?? "Executor",
alertLevel: process.env.ALERT_LEVEL ?? "green",
maxTieFighters: parseInt(process.env.MAX_TIE_FIGHTERS) ?? 72,
};
// Jedi preference handling
function processJediSettings(jediPrefs = {}) {
return {
lightSaberColor: jediPrefs.lightSaberColor ?? "blue",
forceVisions: jediPrefs.forceVisions ?? true,
meditationMode: jediPrefs.meditationMode ?? false,
maxMidichlorians: jediPrefs.maxMidichlorians ?? 0, // 0 is a valid setting!
};
}
Nullish coalescing follows the same support timeline as optional chaining. Node.js 14 brought native support, and the same browsers that support optional chaining also support nullish coalescing.
For older environments, you'll need Babel transpilation:
// babel.config.js
module.exports = {
plugins: [
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
],
};
The real power emerges when you use both operators together. Here's a pattern I've been using heavily in my TypeScript Node.js projects:
// Jedi lightsaber service
class LightsaberService {
async getLightsaberDetails(lightsaberId) {
const lightsaber = await this.findLightsaber(lightsaberId);
return {
id: lightsaber?.id ?? "unknown",
model: lightsaber?.model ?? "Basic Lightsaber",
value: lightsaber?.pricing?.rare ?? lightsaber?.pricing?.standard ?? 0,
description:
lightsaber?.details?.description ??
"An elegant weapon for a more civilized age",
crystals: lightsaber?.components?.crystals?.filter?.(Boolean) ?? [],
isOperational: lightsaber?.status?.powerLevel
? lightsaber.status.powerLevel > 0
: false,
jediForms:
lightsaber?.combatStyles
?.map?.((style) => style?.name)
?.filter?.(Boolean) ?? [],
};
}
}
Here's how I've been building more resilient API clients:
class ImperialAPIClient {
constructor(config = {}) {
this.baseURL = config?.baseURL ?? "https://api.empire.gov";
this.timeout = config?.timeout ?? 5000;
this.retries = config?.retries ?? 3;
this.apiKey = config?.auth?.apiKey ?? process.env.IMPERIAL_API_KEY;
}
async request(system, options = {}) {
const url = `${this.baseURL}${system}`;
const method = options?.method ?? "GET";
const headers = {
"Content-Type": "application/json",
...options?.headers,
};
// Safe authentication handling
const token = options?.auth?.token ?? this.apiKey;
if (token) {
headers.Authorization = `Bearer ${token}`;
}
// Retry logic with optional callbacks
for (let attempt = 1; attempt <= this.retries; attempt++) {
try {
const response = await fetch(url, { method, headers, ...options });
const data = await response.json();
return {
success: response?.ok ?? false,
data: data?.result ?? data,
timestamp: data?.timestamp ?? new Date().toISOString(),
retryCount: attempt - 1,
};
} catch (error) {
options?.onRetry?.(attempt, error);
if (attempt === this.retries) throw error;
await this.delay(attempt * 1000);
}
}
}
}
As someone who has been using TypeScript for most Node.js projects, these operators have felt like a natural extension of the type system. I've been leveraging them since TypeScript 3.7's release, and they integrate beautifully with typed environments:
interface JediProfile {
personal?: {
firstName?: string;
lastName?: string;
homeworld?: {
sector?: string;
planet?: string;
galaxy?: string;
};
};
preferences?: {
lightSaberColor?: "blue" | "green" | "purple" | "red";
forceAbilities?: {
telekinesis?: boolean;
mindTrick?: boolean;
};
};
}
function formatJediProfile(profile: JediProfile): string {
const firstName = profile?.personal?.firstName ?? "";
const lastName = profile?.personal?.lastName ?? "";
const planet = profile?.personal?.homeworld?.planet ?? "Unknown";
return `${firstName} ${lastName} from ${planet}`.trim();
}
The type inference works beautifully - TypeScript understands that optional chaining can return undefined, while nullish coalescing provides the fallback type.
I've been tracking performance across my TypeScript projects since adopting these operators, and the results have been consistently impressive:
Native Node.js 14 performance is excellent - there's virtually no overhead compared to manual null checking. The transpiled versions (when targeting older environments) are still significantly faster than library alternatives like Lodash's get function.
Here are some optimization patterns I've learned:
// Efficient: Minimal chaining depth
const value = jedi?.padawan?.name ?? "Unknown";
// Less efficient: Excessive chaining
const value =
deathStar?.reactorCore?.primarySystems?.powerGrid?.mainRelay?.status
?.operational ?? "unknown";
// Better: Break down complex chains
const powerGrid = deathStar?.reactorCore?.primarySystems?.powerGrid;
const value = powerGrid?.mainRelay?.status?.operational ?? "unknown";
If you're working with Node.js applications and considering adopting these features, here's the approach I've been taking:
Start with the most problematic property access patterns - those deep object traversals that frequently cause TypeErrors in production.
Replace || operators with ?? where you need to preserve falsy values.
Look for verbose conditional logic that can be simplified.
Fine-tune usage in hot code paths.
// Before: Multiple defensive patterns
function loadDeathStarConfig(imperialConfig) {
const config = {};
if (
imperialConfig &&
imperialConfig.deathStar &&
imperialConfig.deathStar.powerLevel !== undefined
) {
config.powerLevel = imperialConfig.deathStar.powerLevel;
} else {
config.powerLevel = 100;
}
config.shieldsUp = !!(
imperialConfig &&
imperialConfig.deathStar &&
imperialConfig.deathStar.shieldsUp
);
return config;
}
// After: Clean and clear
function loadDeathStarConfig(imperialConfig) {
return {
powerLevel: imperialConfig?.deathStar?.powerLevel ?? 100,
shieldsUp: imperialConfig?.deathStar?.shieldsUp ?? false,
};
}
These features represent a maturation of JavaScript as a language. Having used them extensively in TypeScript, I can attest they solve real problems that developers face daily, without adding unnecessary complexity. The fact that TypeScript provided early access and Node.js 14 shipped with native implementation shows the ecosystem rallying around genuinely transformative improvements.
I'm already seeing rapid adoption across the JavaScript community. The benefits are immediate and obvious, the learning curve is minimal, and the performance is excellent.
As someone who has been building primarily Node.js and TypeScript applications with these operators for months, I can confidently say they've fundamentally changed how I write code. They make null-safe programming feel natural rather than defensive.
Now that both operators are officially standardized and natively supported, I'm excited to see the broader JavaScript community embrace these patterns. The combination of both operators creates powerful techniques for handling uncertain data structures - exactly what we need when building resilient applications that interact with external APIs, process user input, and handle configuration.
If you're using Node.js 14 or TypeScript 3.7+, these operators will transform your development experience. I've been using them for months in TypeScript, and now with native Node.js support, the entire ecosystem can benefit from these patterns.
The JavaScript landscape is evolving rapidly, and optional chaining and nullish coalescing represent some of the most practical improvements we've seen in years. 2020 is the year these features moved from "TypeScript exclusive" to "universal JavaScript tools" - and I'm thrilled to see what patterns emerge as the entire community embraces them.
Have you started using optional chaining and nullish coalescing in your Node.js projects? I'd love to hear about your experiences and any interesting patterns you've discovered. Hit me up on Twitter - I'm always excited to discuss modern JavaScript techniques!