JavaScript ES6 Features You Actually Use in 2026 (With Code)

The ES6 features I still use daily are let and const, arrow functions, template literals, destructuring, default parameters, modules, and promises with async/await. Classes, spread, and rest round it out. Everything else is optional. Skip var entirely, and reach for newer syntax only when it keeps the code readable.
Part of the complete web development guide.
On this page
- Why ES6 still matters in 2026
- let and const: stop using var
- Arrow functions (and when not to use them)
- Template literals
- Destructuring
- Spread and rest
- Modules: import and export
- Promises, async/await, and where we are now
- Classes and inheritance
- Maps, Sets, and other collections
- Default, rest, and computed properties
- What’s changed since 2015
- How to modernize a legacy codebase
- FAQ
- Do I still need Babel or TypeScript to use ES6?
- Should I use classes or functions in 2026?
- What’s the fastest way to start using ES6 in an old project?
Most “ES6 features” posts were written in 2016 and never touched again. Meanwhile, the way we actually ship JavaScript changed a lot. Here’s what still matters in 2026, what’s been superseded, and how to use these features without turning your code into a puzzle.
I write a lot of JavaScript for client work, from small business sites to full apps, and these are the features I reach for daily. If you want the practical version instead of a list of syntax trivia, keep reading.

Why ES6 still matters in 2026
ES6 (officially ECMAScript 2015) was the biggest single change to the language. Ten years later, the features it introduced are just… JavaScript. They’re in every tutorial, every framework, every codebase you’ll inherit.
The floor moved too. You no longer need Babel to run this stuff in browsers. Any browser released in the last several years supports ES6 natively, and modern bundlers and runtimes (Vite, esbuild, Node, Deno, Bun) handle it without a transpile step. So the question isn’t “should I use ES6?” It’s “which features pay off, and which ones hurt readability?”

let and const: stop using var
This is the one change with zero downside. var is function-scoped and hoists in ways that cause real bugs. let and const are block-scoped, which matches how you actually think about your code.
// Bug waiting to happen with var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 3, 3, 3
// Fixed with let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 0, 1, 2
Default to const. Reach for let when you’re reassigning. Never type var again. Note that const doesn’t make objects immutable, it just locks the binding:
const user = { name: "Ken" };
user.name = "Kenneth"; // fine, this works
user = {}; // TypeError: assignment to constant variable
Arrow functions (and when not to use them)
Arrow functions are shorter and they don’t rebind this, which solves a whole class of callback headaches.
const prices = [10, 25, 40];
const withTax = prices.map(p => p * 1.0875);
But arrows are not a universal replacement. They have no this of their own, so they’re wrong for object methods that need to reference the object, and wrong for anything you want to use as a constructor.
const counter = {
count: 0,
// Broken: `this` here is not the counter object
increment: () => { this.count++; },
// Correct
incrementFixed() { this.count++; }
};
My rule: arrows for callbacks and short expressions, regular functions for methods and anything that needs its own this.
Template literals
String concatenation is dead. Template literals handle multi-line strings and interpolation without the + soup.
const name = "Ada";
const role = "engineer";
const msg = `Hi ${name}, you're listed as a ${role}.
This is a second line with no escape characters.`;
They also make HTML fragments readable, which matters if you’re building markup in JS. Just be careful with user input: template literals do not escape anything, so never interpolate raw user data into HTML without sanitizing it.
Destructuring
Destructuring is the feature I’d miss most if it disappeared. It pulls values out of objects and arrays cleanly, and it makes function signatures self-documenting.
const config = { host: "localhost", port: 8080, secure: true };
const { host, port, secure = false } = config;
// Rename and set defaults
const { host: serverHost, timeout = 5000 } = config;
// In function parameters
function connect({ host, port, secure = true }) {
return `${secure ? "https" : "http"}://${host}:${port}`;
}
Array destructuring is great for swapping values and handling returned tuples:
let a = 1, b = 2;
[a, b] = [b, a];
const [first, ...rest] = [1, 2, 3, 4];
// first = 1, rest = [2, 3, 4]
Spread and rest
Same syntax, opposite jobs. Spread expands an iterable. Rest collects remaining items.
// Spread: copy and merge without mutating
const defaults = { theme: "light", lang: "en" };
const userPrefs = { theme: "dark" };
const settings = { ...defaults, ...userPrefs };
// { theme: "dark", lang: "en" }
// Rest: variadic functions
function sum(...nums) {
return nums.reduce((total, n) => total + n, 0);
}
One gotcha worth knowing: spread does a shallow copy. Nested objects are still shared by reference.
const original = { user: { name: "Ken" } };
const copy = { ...original };
copy.user.name = "Changed";
console.log(original.user.name); // "Changed" - same nested object
For deep copies, use structuredClone(), which is now standard in browsers and Node. It handles nested objects, arrays, Dates, Maps, and Sets, though not functions or DOM nodes.
const deep = structuredClone(original);
Modules: import and export
ES modules replaced the old script-tag global soup. This is the single biggest structural improvement in the list, and in 2026 it’s the default everywhere: browsers via type="module", Node via .mjs or "type": "module" in package.json.
// math.js
export const add = (a, b) => a + b;
export default function multiply(a, b) { return a * b; }
// app.js
import multiply, { add } from "./math.js";
Two things that trip people up. First, modules are strict mode by default and deferred, so you can’t rely on load order the way you did with classic scripts. Second, named exports are generally easier to refactor and tree-shake than default exports, so I lean named unless there’s a reason not to.
Promises, async/await, and where we are now
Promises were ES6. async/await came in ES2017, but nobody separates them in practice, so they belong here. Async functions let you write asynchronous code that reads top to bottom.
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error("Failed to load user:", err);
throw err;
}
}
In 2026 you’d usually run independent requests in parallel rather than awaiting them one at a time:
const [user, posts] = await Promise.all([
fetch("/api/user").then(r => r.json()),
fetch("/api/posts").then(r => r.json())
]);
And Promise.allSettled is the safer choice when you don’t want one failure to kill the whole batch:
const results = await Promise.allSettled(urls.map(u => fetch(u)));
const ok = results.filter(r => r.status === "fulfilled");
Classes and inheritance
ES6 classes are mostly syntax over the prototype system, but they’re cleaner and they play well with frameworks. They also give you extends, super, and static methods without hand-rolling prototype chains.
class ApiError extends Error {
constructor(message, status) {
super(message);
this.name = "ApiError";
this.status = status;
}
get isServerError() {
return this.status >= 500;
}
}
Worth saying out loud: if you’re writing React or most modern frontend code, you’ll use plain functions and hooks, not classes. Classes still earn their keep for things like custom errors, data models, and library code.
Maps, Sets, and other collections
Map and Set are underused. A Map allows keys of any type and preserves insertion order. A Set gives you fast deduplication.
const seen = new Set([1, 2, 2, 3, 3, 3]);
console.log([...seen]); // [1, 2, 3]
const byId = new Map();
byId.set(42, { name: "Ada" });
byId.get(42); // { name: "Ada" }
If you’re storing key-value data with non-string keys, or you care about order, use a Map instead of an object literal.
Default, rest, and computed properties
Small quality-of-life wins that add up:
// Default parameters
function greet(name = "friend") {
return `Hello, ${name}`;
}
// Computed property names
const field = "email";
const form = { [field]: "ken@example.com" };
What’s changed since 2015
A few ES6-era recommendations are now outdated:
- Transpiling everything is unnecessary. Modern build tools target current browsers by default. Only transpile what you actually need to support.
- Optional chaining and nullish coalescing (
?.and??) came later but replace a ton of defensive&&chains. Use them. structuredClonereplaced the old JSON parse/stringify deep-copy hack for most cases.- Top-level await works in modules now, no wrapper function needed.
How to modernize a legacy codebase
You don’t rewrite everything. You make the new code good and let the old code shrink over time.
- Turn on a linter with
no-varandprefer-construles. Let it flag the work. - Convert files to ES modules when you touch them for another reason.
- Replace
varwithlet/constfile by file, not all at once. - Don’t refactor working code just for style. Refactor when you’re already in there.
That last point matters more than it sounds. I’ve watched projects stall because someone decided to “modernize” a stable codebase and broke things that were fine. The goal is code that’s easy to change, not code that matches a style guide.
If you’re weighing a bigger rebuild against incremental cleanup, that’s usually a conversation worth having early. I do custom web development and spend a fair amount of time talking people out of rewrites they don’t need.
FAQ
Do I still need Babel or TypeScript to use ES6?
No. Every browser and runtime you’d realistically target supports ES6 natively. TypeScript is still worth it for type safety, but it’s a separate decision from ES6 support. You only need a transpiler if you’re targeting very old browsers or using experimental syntax.
Should I use classes or functions in 2026?
Functions, most of the time. Modern frameworks are function-first, and plain functions are easier to test and compose. Use classes for custom errors, data models, and library code where inheritance genuinely helps.
What’s the fastest way to start using ES6 in an old project?
Add a linter with no-var and prefer-const enabled, then fix files as you touch them. Start with modules and let/const, since those change the most bugs. Leave the rest alone until you have a reason to be in that file.


