Modern JavaScript in 2025: Essential Features Every Developer Should Know
Welcome to Devtraverse, your new hub for practical, in-depth development tutorials and tech insights! I'm excited to kick off this blog with the first article in a series exploring the most impactful JavaScript features that are transforming how we build applications in 2025.
JavaScript continues to evolve at a rapid pace, with new features that make our code more robust, readable, and maintainable. In this series, we'll explore everything from syntax improvements that prevent common errors to powerful new APIs that solve long-standing JavaScript limitations.
This first article focuses on two game-changing features that can immediately improve your code quality: Optional Chaining and Nullish Coalescing. These seemingly small syntax improvements can dramatically reduce bugs and make your code more readable.
Why These Features Matter
If you've been working with JavaScript for any length of time, you've likely encountered these scenarios:
You try to access a deeply nested property in an object, but one of the intermediate properties is
undefined
, causing your app to crash with the infamousCannot read property 'x' of undefined
error.You use the logical OR operator (
||
) to provide default values, but it treats falsy values like0
, empty strings, andfalse
as "missing," replacing them with your default when that's not what you intended.
These two issues are so common that they've spawned countless Stack Overflow questions, utility functions, and even entire libraries dedicated to solving them. Finally, JavaScript has built-in syntax to handle these cases elegantly.
Optional Chaining: Preventing "Cannot Read Property" Errors
Optional chaining, denoted by the ?.
operator, allows you to safely access nested properties without having to manually check each level in the chain. It automatically returns undefined
if any part of the chain is null
or undefined
instead of throwing an error.
The Problem Optional Chaining Solves
Consider a typical scenario where you're accessing nested data from an API response:
// User data from an API
const user = {
id: 42,
name: "Alex Kim",
// profile might not be included in all responses
};
// Without optional chaining
function getProfileImage(user) {
// Verbose defensive coding
if (user && user.profile && user.profile.images &&
user.profile.images.avatar) {
return user.profile.images.avatar;
}
return 'default-avatar.png';
}
javascript
This defensive approach is verbose and easy to get wrong. Missing a check in a long chain can lead to runtime errors.
Using Optional Chaining
Here's the same function with optional chaining:
// With optional chaining
function getProfileImage(user) {
return user?.profile?.images?.avatar || 'default-avatar.png';
}
javascript
This is dramatically more concise and just as safe. If any part of the chain (user
, profile
, images
) is null
or undefined
, the expression short-circuits and returns undefined
instead of throwing an error.
Beyond Object Properties
Optional chaining isn't just for object properties. It works in three key contexts:
1. Object Properties (as shown above)
const avatarUrl = user?.profile?.images?.avatar;
javascript
2. Function Calls
// Only calls calculateDiscount if it exists
const discount = cart.calculateDiscount?.();
// With arguments
const formattedPrice = priceFormatter?.format(price, currency);
javascript
Without optional chaining, you'd need to check if the function exists before calling it:
// The old way
const discount = typeof cart.calculateDiscount === 'function'
? cart.calculateDiscount()
: undefined;
javascript
3. Array Elements
// Safely access array elements, even if firstUser might be undefined
const userName = users?.[0]?.name;
// Dynamic property access
const dynamicValue = object?.[propertyName];
javascript
Best Practices for Optional Chaining
While optional chaining is powerful, it should be used judiciously:
Use for truly optional paths: When a property might legitimately be absent (like API responses or user inputs)
Don't overuse: If a property should always be present, and its absence indicates a bug, it's better to let the error happen so you can fix the underlying issue
Combine with TypeScript: For even better safety, TypeScript's type checking can help you distinguish between truly optional properties and programming errors
Nullish Coalescing: Setting Defaults the Right Way
The nullish coalescing operator (??
) provides a more precise way to provide default values than the logical OR operator (||
).
The Problem Nullish Coalescing Solves
JavaScript developers have long used the logical OR operator to provide default values:
// Setting defaults with logical OR
function createUserConfig(options) {const config = {
username: options.username || 'anonymous',
volume: options.volume || 100,
darkMode: options.darkMode || true,
notifications: options.notifications || 'all'
};
return config;
}
javascript
This has a major flaw: the logical OR considers any falsy value (including 0
, empty strings, and false
) as a reason to use the default. This leads to subtle bugs where:
If a user explicitly sets
volume
to0
, they get100
insteadIf a user explicitly sets
darkMode
tofalse
, they gettrue
insteadIf a user explicitly sets
notifications
to an empty string, they get'all'
instead
Using Nullish Coalescing
The nullish coalescing operator only defaults when the left side is specifically null
or undefined
:
// Setting defaults with nullish coalescing
function createUserConfig(options) {
const config = {
username: options.username ?? 'anonymous',
volume: options.volume ?? 100,
darkMode: options.darkMode ?? true,
notifications: options.notifications ?? 'all'
};
return config;
}
javascript
Now, any explicitly provided value, even if falsy, is respected:
If
volume
is0
, it stays0
(not replaced with100
)If
darkMode
isfalse
, it staysfalse
(not replaced withtrue
)If
notifications
is an empty string, it stays as an empty string (not replaced with'all'
)
When to Use Nullish Coalescing vs. Logical OR
Use
??
when you want to respect all explicitly set values (including falsy ones)Use
||
when you specifically want to override falsy values with a default
Here's a practical comparison:
// Looking at different values with || vs ??
function compare(value) {console.log(`Original: ${value}`);console.log(`With ||: ${value || 'default'}`);
console.log(`With ??: ${value ?? 'default'}`);
console.log('---');
}
compare(null); // || and ?? both return 'default'
compare(undefined); // || and ?? both return 'default'
compare(''); // || returns 'default', ?? returns ''
compare(0); // || returns 'default', ?? returns 0
compare(false); // || returns 'default', ?? returns false
compare(NaN); // || returns 'default', ?? returns NaN
compare('Hello'); // || and ?? both return 'Hello'
javascript
Combining Features for Powerful Patterns
Optional chaining and nullish coalescing work beautifully together for safe property access with meaningful defaults:
// Safe access with meaningful defaults
function getUserDisplayData(user) {return {
name: user?.name ?? 'Anonymous',
avatar: user?.profile?.images?.avatar ?? 'default-avatar.png',
role: user?.settings?.role ?? 'user',
theme: user?.preferences?.theme ?? 'light',
notifications: user?.preferences?.notifications ?? true
};
}
// Works safely with any of these:
getUserDisplayData({ name: 'Alex' });
getUserDisplayData({ name: 'Taylor', preferences: { theme: 'dark' } });
getUserDisplayData(null);
javascript
This pattern is especially valuable when working with:
API responses that may have varying structures
User inputs that might be incomplete
Configuration objects with optional settings
Feature flags or conditional data structures
Real-World Examples
Let's look at some practical applications to see how these features improve real-world code.
Simplified API Data Handling
Before:
function processResponse(response) {
const data = response && response.data ? response.data : {};
const items = data.items && Array.isArray(data.items) ? data.items : [];
const count = data.metadata && typeof data.metadata.count === 'number' ? data.metadata.count : 0;
const results = items.map(item => {
const id = item && item.id;
const title = item && item.title ? item.title : 'Untitled';
const thumbnailUrl = item &&
item.media &&
item.media.thumbnails &&
item.media.thumbnails.small ?
item.media.thumbnails.small :
'default-thumbnail.jpg';
return { id, title, thumbnailUrl };
});
return { results, count };
}
javascript
After:
function processResponse(response) {
const items = response?.data?.items ?? [];
const count = response?.data?.metadata?.count ?? 0;
const results = items.map(item => ({
id: item?.id,
title: item?.title ?? 'Untitled',
thumbnailUrl: item?.media?.thumbnails?.small ?? 'default-thumbnail.jpg'
}));
return { results, count };
}
javascript
Configuration Management
Before:
function initializeApp(config) {
// Check if config exists
if (!config) config = {};
// API settings
const apiUrl = config.api && config.api.url
? config.api.url
: 'https://api.default.com';
const apiVersion = config.api && config.api.version
? config.api.version
: 'v1';
const apiTimeout = config.api && config.api.timeout
? config.api.timeout
: 30000;
// User preferences
const theme = config.user && config.user.theme
? config.user.theme
: 'light';
const language = config.user && config.user.language
? config.user.language
: 'en';
// Feature flags
const enableExperimentalFeatures = config.features &&
config.features.experimental
? config.features.experimental
: false;
// ... and so on
}
javascript
After:
function initializeApp(config = {}) {
// API settings
const apiUrl = config?.api?.url ?? 'https://api.default.com';
const apiVersion = config?.api?.version ?? 'v1';
const apiTimeout = config?.api?.timeout ?? 30000;
// User preferences
const theme = config?.user?.theme ?? 'light';
const language = config?.user?.language ?? 'en';
// Feature flags
const enableExperimentalFeatures = config?.features?.experimental
?? false;
// ... much cleaner!
}
javascript
Plugin System with Method Checking
function invokePluginMethod(pluginName, methodName, ...args) {
// Old approach
/*
const plugin = this.plugins[pluginName];
if (plugin && typeof plugin[methodName] === 'function') {
return plugin[methodName](...args);
}
return null;
*/
// With optional chaining
return this.plugins[pluginName]?.[methodName]?.(...args) ?? null;
}
javascript
Browser and Node.js Support
Both optional chaining and nullish coalescing have excellent browser and runtime support in 2025. They're safe to use in all modern environments.
Browser Support:
Chrome: 80+
Firefox: 72+
Safari: 13.1+
Edge: 80+
Node.js Support:
Node.js: 14.0.0+
For legacy environments, transpilers like Babel can convert these features to compatible code.
Wrapping Up
Optional chaining and nullish coalescing are small changes with a big impact. They make your code:
More robust by preventing common types of runtime errors
More readable by removing verbose conditional checks
More maintainable by clearly expressing intent
The best part is that you can start using these features immediately in almost any JavaScript project. They require no framework or library support—just modern JavaScript.
In the next article in this series, we'll explore modern array methods that make data manipulation more elegant and functional. We'll see how methods like Array.at()
, Array.flatMap()
, and the new non-mutating array methods improve your code's clarity and safety.
What JavaScript features would you like to see covered in this series? Did you find these features helpful in your own code? Share your experiences in the comments below!