ES6 — officially ES2015 — was the release that reshaped JavaScript. Almost every feature people now consider ordinary arrived in it or shortly after: let and const, arrow functions, template literals, destructuring, classes, promises and modules. This guide covers what changed and why, then the useful additions of the years since.
let and const#
var oldStyle = "function scoped";
let changeable = "block scoped";
const fixed = "block scoped, cannot be reassigned";
The problem var caused is clearest in a loop:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 3, 3, 3 - one shared i, already finished
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 0, 1, 2 - a new i for each iteration
Default to const, use let when the value genuinely changes, and treat var as legacy.
Arrow functions#
const double = (n) => n * 2;
const add = (a, b) => a + b;
const greet = () => "hello";
const makeUser = (name) => ({ name }); // brackets to return an object
Shorter, but the real difference is this. An arrow function does not create its own, so it uses the one from the surrounding scope:
const timer = {
seconds: 0,
startBroken() {
setInterval(function () {
this.seconds++; // this is not the timer object
}, 1000);
},
startWorking() {
setInterval(() => {
this.seconds++; // this is the timer object
}, 1000);
},
};
That behaviour removed an entire category of bug — and an entire generation of const self = this workarounds.
Template literals#
const name = "Ana";
const age = 34;
const message = "Hello " + name + ", you are " + age; // before
Backtick-quoted strings can embed expressions and span lines, which replaced most string concatenation overnight. They also made multi-line HTML fragments readable without escape characters.
Destructuring#
const [first, second, ...rest] = [1, 2, 3, 4, 5];
const { name, age, city = "unknown" } = person;
const { address: { postcode } = {} } = person;
// In parameters
function describe({ name, age = 0 }) {
return name + " is " + age;
}
// Swapping without a temporary variable
let a = 1, b = 2;
[a, b] = [b, a];
Spread and rest#
const combined = [...first, ...second];
const copy = [...original];
const merged = { ...defaults, ...userSettings };
const updated = { ...user, name: "Ana" };
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
Math.max(...[3, 9, 2]); // spread into arguments
The same three dots do two jobs: collecting into an array when used in a parameter list, and expanding out of one everywhere else.
Default parameters#
function greet(name, greeting = "Hello") {
return greeting + ", " + name;
}
Before this, every function began with a line checking whether an argument was undefined.
Classes#
class Animal {
#id = crypto.randomUUID(); // truly private
constructor(name) {
this.name = name;
}
speak() {
return this.name + " makes a sound";
}
static create(name) {
return new Animal(name);
}
}
class Dog extends Animal {
speak() {
return this.name + " barks";
}
}
This is syntax over JavaScript’s existing prototype system rather than a new object model — but it is far more readable than what it replaced. The # prefix for private fields came later and is genuinely private, unlike the underscore convention.
Promises, then async and await#
// Callbacks - the thing being replaced
getUser(id, function (error, user) {
if (error) return handle(error);
getOrders(user.id, function (error, orders) {
if (error) return handle(error);
// and so on
});
});
// Promises - flat instead of nested
getUser(id)
.then(user => getOrders(user.id))
.then(orders => console.log(orders))
.catch(handle);
// async/await - reads like ordinary code
async function loadOrders(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
return orders;
} catch (error) {
handle(error);
}
}
Async and await are the single largest readability improvement in modern JavaScript. Note that awaiting in sequence is often not what you want:
// Sequential - the total is the sum
const a = await fetchA();
const b = await fetchB();
// Concurrent - the total is the slowest
const [a, b] = await Promise.all([fetchA(), fetchB()]);
Modules#
// helpers.js
export function clean(text) { return text.trim(); }
export default function main() { }
// app.js
import main, { clean } from "./helpers.js";
import * as helpers from "./helpers.js";
JavaScript had no official module system before this. In the browser, add type="module" to the script tag; in Node, set "type": "module" in package.json.
The useful additions since#
// Optional chaining and nullish coalescing (2020)
const city = user?.address?.city ?? "unknown";
options.retries ??= 3;
// Array and object helpers
[1, [2, [3]]].flat(2); // [1, 2, 3]
Object.entries(obj); // [[key, value], ...]
Object.fromEntries(pairs);
[3, 1, 2].at(-1); // 2
// Non-mutating array methods
const sorted = items.toSorted((a, b) => a - b);
const reversed = items.toReversed();
const replaced = items.with(0, "new");
// String helpers
" x ".trimStart();
"abc".replaceAll("a", "b");
"5".padStart(3, "0"); // "005"
// Top-level await, in modules
const data = await fetch("/api").then(r => r.json());
// Grouping
Object.groupBy(items, item => item.category);
The non-mutating array methods are worth adopting deliberately. sort() and reverse() change the original array in place, which is a recurring source of surprising bugs; toSorted() and toReversed() do not.
Do you need a build step?#
For plain JavaScript in modern browsers, no. Write modules, load them with type="module", and it works.
You need one for TypeScript, for JSX, for bundling many files into fewer requests, or for supporting browsers that predate these features. Vite is the usual choice and needs very little configuration.
Questions people ask#
What do ES6, ES2015 and ECMAScript mean?
ECMAScript is the standard JavaScript implements. ES6 and ES2015 are the same release under two names — the committee moved to year-based naming afterwards, so ES2016 onwards are annual.
Do I still need Babel?
Only for old browsers or non-standard syntax such as JSX. Modern browsers support everything here natively.
Are classes better than functions and prototypes?
They are the same mechanism with clearer syntax. Plenty of good JavaScript uses plain objects and functions instead; neither is wrong.
Should I use semicolons?
Unchanged by any of this. Pick a style and let a formatter such as Prettier apply it consistently.
Where to go next#
- JavaScript basics — the syntax in full.
- Asynchronous JavaScript — promises and async in depth.
- Arrays and objects in JavaScript — destructuring and spread in context.