ES6 Features You Actually Use in 2026 (and How to Write Them)

The ES6 features that still earn their keep in 2026 are let and const for block scoping, arrow functions, template literals, destructuring, default parameters, and modules. I use const by default, reach for let only when reassigning, and treat var as a bug waiting to happen.
Part of the complete web development guide.
On this page
- ES6 isn’t new. That’s the point.
- let and const (and why var is a smell)
- Arrow functions: great, with one real gotcha
- Template literals and tagged templates
- Destructuring, defaults, and the spread operator
- Modules: import and export
- Promises, async/await, and the part nobody mentions
- Classes: fine, but not always the answer
- What got replaced or was never that useful
- How to actually learn this stuff
- FAQ
- Do I still need a build tool to use ES6 in 2026?
- Is const really better than let?
- Should I use async/await everywhere or stick with Promises?
Most “best ES6 features” posts are written like a feature list from 2016. Ten years later, half of that advice is either baked into every runtime or quietly wrong. Here’s what actually shows up in modern codebases, why it matters, and the mistakes I still see when reviewing JavaScript.

ES6 isn’t new. That’s the point.
ES6 shipped in 2015. It’s not a cutting edge thing you adopt anymore. It’s the baseline. Every browser, every Node version, and every build tool assumes it. If you’re still reading a tutorial that calls arrow functions “the future of JavaScript,” close the tab.
What’s actually useful now is knowing which features earn their keep, which ones cause bugs, and which ones got superseded by newer syntax. That’s what this covers.

let and const (and why var is a smell)
Block scoping is the single biggest reason ES6 code behaves better than ES5 code. var leaks out of loops and conditionals. let and const don’t.
// var: the classic bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs 3, 3, 3
// let: fixed
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs 0, 1, 2
Use const by default. Switch to let only when you actually reassign. const doesn’t make objects immutable, it just stops you from rebinding the name, which is still worth it because it kills a whole class of accidental reassignment bugs.
Arrow functions: great, with one real gotcha
Arrow functions don’t have their own this. They inherit it from the enclosing scope. That’s usually what you want, especially in callbacks and array methods. It’s the reason this works:
class Timer {
constructor() {
this.seconds = 0;
}
start() {
setInterval(() => {
this.seconds++; // `this` is the Timer instance
}, 1000);
}
}
The gotcha: you can’t use an arrow function as a method that needs its own this, and you can’t use arguments inside one. If you need a dynamic this, use a regular function. This trips people up in event handlers and object methods constantly.
Template literals and tagged templates
String concatenation with + is mostly gone, and good riddance. Template literals handle multi-line strings and interpolation cleanly.
const user = { name: "Dana", plan: "pro" };
const msg = `Hi ${user.name}, your ${user.plan} plan renews soon.`;
The underused part is tagged templates. A tag function receives the literal parts and the interpolated values separately, which is how libraries like styled-components and GraphQL’s gql work. You can also use it to escape user input:
function safe(strings, ...values) {
return strings.reduce((out, str, i) => {
const val = values[i - 1];
const escaped = String(val)
.replace(/&/g, "&")
.replace(//g, ">");
return out + escaped + str;
});
}
Destructuring, defaults, and the spread operator
Destructuring cut a lot of boilerplate. It’s everywhere now, and it’s worth being fluent in.
const config = { host: "localhost", port: 5432, ssl: false };
// object destructuring with defaults and rename
const { host, port = 3000, ssl: useSsl = true } = config;
// array destructuring
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// spread for shallow copies
const updated = { ...config, port: 5433 };
// rest params in functions
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
One warning that matters in 2026: spread does a shallow copy. If your object has nested objects, they’re shared by reference. This bites people in React state updates constantly. Use structuredClone() for a real deep copy, which is now available in Node and all modern browsers.
const original = { user: { name: "Dana" }, tags: ["a"] };
const deep = structuredClone(original);
deep.user.name = "Sam";
console.log(original.user.name); // still "Dana"
Modules: import and export
ES modules replaced the old script-tag globals and the CommonJS require pattern in most new code. Native ESM works in browsers and Node (with "type": "module" in package.json or an .mjs extension).
// 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";
The thing that still confuses people: default exports are harder to rename safely during refactors, and mixing default and named exports in one file is a code review smell. I lean toward named exports almost everywhere. It makes tooling and autocomplete work better.
Promises, async/await, and the part nobody mentions
Promises were ES6. async/await came in ES2017. But they’re used together so often that treating them separately is pointless. The real skill is knowing when await is slowing you down.
// Sequential: slow, waits for each one
const a = await fetchUser(1);
const b = await fetchUser(2);
// Parallel: fast, both start at once
const [a2, b2] = await Promise.all([fetchUser(1), fetchUser(2)]);
If two requests don’t depend on each other, Promise.all them. This is one of the most common performance mistakes I see, and it has nothing to do with ES6 syntax and everything to do with understanding it. Also remember Promise.allSettled when you don’t want one failure to kill the whole batch.
Classes: fine, but not always the answer
ES6 classes are mostly syntactic sugar over prototypes. They’re useful for clear object shapes and inheritance, but JavaScript’s real strength is composition. For a lot of app code, plain objects and functions beat a class hierarchy, especially in a functional-heavy framework like modern React.
class ApiClient {
#baseUrl; // private field, ES2022, not ES6
constructor(baseUrl) {
this.#baseUrl = baseUrl;
}
async get(path) {
const res = await fetch(`${this.#baseUrl}${path}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
}
Note the #baseUrl. Private fields are not ES6. They landed years later. If a tutorial tells you ES6 gave us private class fields, it’s wrong. This is the kind of thing that separates a real reference from a content farm.
What got replaced or was never that useful
SymbolandMap/Setare genuinely useful, but the “you should use Map instead of objects” advice is oversold. Plain objects are fine for most cases.- Generators and iterators are powerful but rare in app code. You’ll see them in libraries, not in your average component.
- Proxy and Reflect are niche. Useful for metaprogramming, overkill for normal work.
- Default parameters, rest, and spread are so common now that people forget they were ES6 additions.
How to actually learn this stuff
The best way isn’t reading feature lists. It’s writing real code and noticing when your syntax is fighting you. If you maintain a site or an app and the JavaScript feels brittle, a lot of it usually traces back to scoping, this, or async handling rather than anything exotic. When I take on custom web development work, those three areas account for most of the cleanup.
Read the MDN docs, not blog posts from a decade ago. Run code in your console. And when a “best ES6 features” article shows up in search results, check the date. If it’s older than a couple of years and hasn’t been updated, the advice has probably aged.
FAQ
Do I still need a build tool to use ES6 in 2026?
No. Modern browsers and Node support ES6 and most later features natively. You still want a bundler for module resolution, minification, and TypeScript, but you don’t need one just to write arrow functions or let.
Is const really better than let?
As a default, yes. It prevents accidental rebinding and signals intent. But it doesn’t freeze objects or arrays, so don’t treat it as immutability. Use Object.freeze or immutable patterns when you actually need that.
Should I use async/await everywhere or stick with Promises?
Use async/await for readability, but drop back to Promise.all, Promise.allSettled, or Promise.race when you need concurrency or race logic. They’re not competing tools. They work together.


