ES6 & the Modern Era
ES2015 — universally known as ES6 — is the release that turned JavaScript from a small scripting language into a serious application language. After years of stagnation following ES5 (2009), ES6 landed a huge batch of features all at once: block scoping, arrow functions, classes, modules, promises, and more. Almost everything people mean by “modern JavaScript” traces back to this single specification, and the language has shipped a smaller, predictable update every year since.
Why ES6 matters
Before ES6, JavaScript had only var, function-based scope, callback-driven async code, and no native module system or class syntax. Large codebases leaned on libraries and patterns to fill those gaps. ES6 standardized the answers directly in the language, which is why it remains the dividing line between “legacy” and “modern” code. The committee (TC39) also switched to a yearly, feature-at-a-time cadence — so later versions (ES2016 through ES2023+) are incremental, while ES6 was the watershed.
The headline features
ES6 is large, but a handful of features account for most of its day-to-day impact.
Block scoping with let and const. These replace var with predictable, block-scoped bindings and no surprising hoisting behavior. const prevents reassignment, signaling intent.
const MAX = 100; // cannot be reassigned
let total = 0; // block-scoped, reassignable
for (let i = 0; i < 3; i++) {
// each iteration gets its own `i`
}
// console.log(i); // ReferenceError — i is not visible here
Arrow functions give a concise syntax and lexical this, eliminating the classic const self = this workaround in callbacks.
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2);
console.log(doubled);
Output:
[ 2, 4, 6 ]
Template literals add string interpolation and multi-line strings using backticks.
const user = "Ada";
const greeting = `Hello, ${user}!
Welcome back.`;
console.log(greeting);
Output:
Hello, Ada!
Welcome back.
Destructuring, defaults, rest, and spread make pulling values out of objects and arrays — and passing them around — far more expressive.
const point = { x: 10, y: 20 };
const { x, y } = point;
const [first, ...rest] = [1, 2, 3, 4];
const merged = { ...point, z: 30 };
console.log(x, y, first, rest, merged);
Output:
10 20 1 [ 2, 3, 4 ] { x: 10, y: 20, z: 30 }
Classes provide clean syntax over the prototype system, including inheritance with extends and super.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
speak() {
return `${this.name} barks`;
}
}
console.log(new Dog("Rex").speak());
Output:
Rex barks
Modules (import / export) standardized how files share code, replacing competing module formats.
// math.js
export const add = (a, b) => a + b;
// app.js
import { add } from "./math.js";
console.log(add(2, 3));
Promises model asynchronous results as first-class values, paving the way for the async/await syntax that arrived in ES2017.
fetch("/api/user")
.then((res) => res.json())
.then((user) => console.log(user.name))
.catch((err) => console.error(err));
Map, Set, and friends add real keyed collections (Map, Set, plus the garbage-collection-friendly WeakMap and WeakSet) instead of abusing plain objects.
const seen = new Set([1, 1, 2, 3]);
console.log([...seen]);
Output:
[ 1, 2, 3 ]
Symbols introduce unique, collision-proof property keys, used heavily for protocol hooks like Symbol.iterator.
Generators (function* with yield) produce values lazily and underpin custom iterables and many async patterns.
function* range(start, end) {
for (let i = start; i < end; i++) yield i;
}
console.log([...range(1, 4)]);
Output:
[ 1, 2, 3 ]
Feature-to-page map
Each major area below has a dedicated page. The version pages cover everything that shipped after ES6, year by year.
| Feature area | Where to read more |
|---|---|
| Destructuring, default/rest/spread | /javascript/destructuring-spread |
ES2016 & ES2017 (**, Array.includes, async/await) | /javascript/es2016-2017 |
ES2018 & ES2019 (object spread, flat, Object.fromEntries) | /javascript/es2018-2019 |
| ES2020 (optional chaining, nullish coalescing, BigInt) | /javascript/es2020 |
ES2021 & ES2022 (replaceAll, top-level await, class fields) | /javascript/es2021-2022 |
ES2023 and beyond (findLast, immutable array methods) | /javascript/es2023-plus |
Naming gotcha: “ES6” and “ES2015” are the same release. After 2015, TC39 dropped sequential numbers in favor of years, so you will rarely hear “ES7” — it is “ES2016.” Use the year-based names to avoid ambiguity.
How versions ship today
A feature is not “ES2020” the day someone proposes it. Proposals move through TC39 stages 0–4; only Stage 4 features are folded into that year’s specification and, by then, are already implemented in browsers and Node. In practice you can adopt new syntax early using a transpiler such as Babel or TypeScript, then drop it once your target runtimes support the feature natively.
Best Practices
- Treat ES6 as your baseline — every evergreen browser and supported Node version ships it, so there is no reason to write ES5-style
varand callback code in new projects. - Default to
const, useletonly when you must reassign, and avoidvarentirely outside legacy maintenance. - Prefer
async/awaitover long promise chains, but understand promises first sinceawaitis sugar over them. - Reach for
Map/Setwhen you need keyed or unique collections instead of overloading plain objects. - Learn destructuring and spread early — they appear in nearly every modern codebase and React/Node API.
- When targeting older runtimes, transpile with Babel or TypeScript rather than hand-writing downlevel code.