JavaScript

ES6+ Features You Should Be Using in 2026

javascript-es6-features

I still review codebases in 2026 that use var everywhere, concatenate strings with +, and write fifteen lines of code to safely access a nested object property. ES6 landed over a decade ago. ES2020 features like optional chaining have been in every major browser for years. There's no excuse anymore.

This isn't a history lesson on ECMAScript versions. It's a practical rundown of the modern JavaScript features that will make your code shorter, safer, and easier to read — features you should be reaching for every single day.

Destructuring: Stop Writing data.user.name

Destructuring lets you unpack values from objects and arrays into individual variables. Once you start using it, going back to the old way physically hurts.

Object Destructuring

// Before: tedious and repetitive
const name = user.name;
const email = user.email;
const role = user.role;

// After: one line
const { name, email, role } = user;

You can rename variables during destructuring, which is essential when property names would conflict:

const { name: userName, email: userEmail } = user;
const { name: companyName } = company;
// userName, userEmail, companyName — no conflicts

You can also set defaults for when a property is missing:

const { theme = 'light', language = 'en' } = userPreferences;
// If userPreferences.theme is undefined, theme defaults to 'light'

Array Destructuring

const [first, second, ...rest] = [10, 20, 30, 40, 50];
// first = 10, second = 20, rest = [30, 40, 50]

// Swap variables without a temp variable
let a = 1, b = 2;
[a, b] = [b, a];
// a = 2, b = 1

Where this really shines is function parameters. Instead of accepting a single options object and accessing properties with options.width, destructure right in the parameter list:

// Clean function signature with defaults
function createThumbnail({ width = 200, height = 200, quality = 80 } = {}) {
  console.log(`Creating ${width}x${height} thumbnail at ${quality}% quality`);
}

createThumbnail({ width: 400 });
// "Creating 400x200 thumbnail at 80% quality"

Spread and Rest: Three Dots, Infinite Uses

The ... operator does two things depending on context. When you spread, you're expanding something. When you rest, you're collecting things.

Spread for Objects

const defaults = { theme: 'light', fontSize: 14, lang: 'en' };
const userSettings = { theme: 'dark', fontSize: 18 };

// Merge objects — later properties override earlier ones
const config = { ...defaults, ...userSettings };
// { theme: 'dark', fontSize: 18, lang: 'en' }

// Shallow clone an object
const copy = { ...original };

// Add/override a single property immutably
const updated = { ...user, lastLogin: new Date() };

I use this pattern constantly in React state management and API handlers. It's clean, immutable, and readable.

Spread for Arrays

const frontend = ['React', 'Vue', 'Svelte'];
const backend = ['Node', 'Deno', 'Bun'];

// Combine arrays
const fullStack = [...frontend, ...backend];

// Clone an array
const copy = [...original];

// Convert NodeList to array (still useful in DOM manipulation)
const elements = [...document.querySelectorAll('.card')];

Rest Parameters

function log(level, ...messages) {
  messages.forEach(msg => console[level](msg));
}

log('warn', 'Disk space low', 'Only 2GB remaining');
// console.warn('Disk space low')
// console.warn('Only 2GB remaining')

Rest replaces the old arguments object, and it's better in every way — it gives you a real array, not an array-like object, and it's explicit about which parameters are collected.

Template Literals: String Concatenation Is Dead

If you're still writing "Hello, " + name + "! You have " + count + " notifications.", please stop. Template literals have been available since 2015.

const message = `Hello, ${name}! You have ${count} notifications.`;

// Multi-line strings — no more \n or array.join
const html = `
  <div class="card">
    <h2>${title}</h2>
    <p>${description}</p>
    <span>${formatDate(date)}</span>
  </div>
`;

// Expressions inside interpolations
const status = `Status: ${isActive ? 'Active' : 'Inactive'}`;
const price = `Total: $${(quantity * unitPrice).toFixed(2)}`;

Tagged template literals are an underused power feature. Libraries like styled-components, lit-html, and graphql-tag all use them. The tag function receives the string parts and interpolated values separately, letting you do custom processing — escaping HTML, building SQL queries safely, or constructing typed queries.

Optional Chaining: No More && Chains

This is the feature that saved the most lines of code in my career. Before optional chaining, accessing deeply nested properties was a minefield:

// The old way — defensive && chaining
const city = user && user.address && user.address.city;

// Slightly better but still ugly
const city = user ? (user.address ? user.address.city : undefined) : undefined;

// The modern way
const city = user?.address?.city;

If any part of the chain is null or undefined, the entire expression short-circuits to undefined. No error. No crash. Just undefined.

It works with methods and bracket notation too:

// Optional method calls
const result = api.getData?.();  // calls getData if it exists

// Optional bracket notation
const value = config?.[dynamicKey];

// Combine with array access
const firstTag = post?.tags?.[0];

I use optional chaining on every API response. You never know when a backend team decides to make a field nullable without telling you.

Nullish Coalescing: Better Defaults

The || operator for default values has a problem: it triggers on any falsy value, including 0, "", and false. That's often not what you want.

// Problem with ||
const count = serverResponse.count || 10;
// If count is 0, this returns 10. That's wrong.

const name = formData.name || 'Anonymous';
// If someone submits an empty string intentionally, this overwrites it.

// Solution: nullish coalescing (??)
const count = serverResponse.count ?? 10;
// Only falls back to 10 if count is null or undefined. 0 stays as 0.

const name = formData.name ?? 'Anonymous';
// Empty string stays as empty string.

Pair it with optional chaining for incredibly concise and safe property access:

const theme = user?.preferences?.theme ?? 'light';
// Safely navigates the chain, defaults to 'light' only if the result is null/undefined

Use ?? when 0, empty string, or false are valid values. Use || when you want to fall back on any falsy value. Mixing them up is one of the most common source of bugs I see in code reviews.

ES Modules: import/export Done Right

CommonJS (require/module.exports) was Node's module system for over a decade. ES modules (import/export) are the standard now, and they work everywhere — browsers, Node.js, Deno, and Bun.

// math.js — named exports
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export const PI = 3.14159;

// app.js — named imports
import { add, multiply, PI } from './math.js';

// Import everything under a namespace
import * as math from './math.js';
console.log(math.add(2, 3));

// Default export (one per module)
// logger.js
export default class Logger {
  log(msg) { console.log(`[LOG] ${msg}`); }
}

// app.js
import Logger from './logger.js';

Key differences from CommonJS that catch people:

Dynamic Imports

When you need to load a module conditionally or lazily:

// Load a heavy module only when needed
async function renderChart(data) {
  const { Chart } = await import('./chart-library.js');
  const chart = new Chart(data);
  chart.render('#chart-container');
}

// Conditional loading
const locale = navigator.language.startsWith('fr')
  ? await import('./locales/fr.js')
  : await import('./locales/en.js');

Dynamic imports return promises, so they work naturally with async/await. Every bundler (Webpack, Vite, Rollup) uses dynamic imports as code-splitting boundaries — call import() and the bundler automatically creates a separate chunk.

Honorable Mentions

A few more features that deserve a spot in your daily toolkit:

Object.entries() and Object.fromEntries()

const prices = { apple: 1.5, banana: 0.75, cherry: 3.0 };

// Transform object values
const discounted = Object.fromEntries(
  Object.entries(prices).map(([fruit, price]) => [fruit, price * 0.9])
);
// { apple: 1.35, banana: 0.675, cherry: 2.7 }

Array.prototype.at()

const colors = ['red', 'green', 'blue'];
colors.at(-1);  // 'blue' — negative indexing, finally!
colors.at(-2);  // 'green'

structuredClone()

const original = { name: 'test', nested: { deep: true }, date: new Date() };
const clone = structuredClone(original);
// True deep clone — handles dates, maps, sets, typed arrays, and more
// No more JSON.parse(JSON.stringify(obj)) hacks

Share Beautiful Code Snippets

Turn your JavaScript code into stunning, shareable images. Perfect for tutorials, documentation, and social media posts.

Try Code to Image →

Modern JavaScript isn't just syntactic sugar. Optional chaining prevents runtime crashes. Nullish coalescing prevents subtle data bugs. Destructuring makes function signatures self-documenting. ES modules enable tree-shaking that shrinks your bundles.

If your codebase is still stuck in ES5 patterns, you're not just writing more code — you're writing buggier, harder-to-maintain code. Pick one feature from this list, start using it in your next commit, and build from there. Your future self (and your code reviewers) will thank you.

Sachin Bhanushali
Written by

Sachin Bhanushali

Full-stack developer and creator of HTMLtoImages. Building free, privacy-first developer tools that run entirely in your browser.