Skip to content
JavaScript js modern 4 min read

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 areaWhere 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 var and callback code in new projects.
  • Default to const, use let only when you must reassign, and avoid var entirely outside legacy maintenance.
  • Prefer async/await over long promise chains, but understand promises first since await is sugar over them.
  • Reach for Map/Set when 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.
Last updated June 1, 2026
Was this helpful?