JavaScript gains new features every year. Knowing roughly when a feature arrived helps you read other people's code, judge whether it will run in the browsers and Node.js versions you support, and spot old workarounds that a built-in now replaces. Use this lesson as a reference rather than reading it top to bottom.
How a feature gets in
The language is standardised as ECMAScript by a committee called TC39. Proposals move through numbered stages, from an idea at stage 0 to finished at stage 4. A feature reaches stage 4 when it has a complete specification, tests and shipping implementations in more than one engine. Each June a new edition is published, named after its year, containing every proposal that reached stage 4 in the preceding months.
Two consequences:
- Browsers often ship a feature before the edition that contains it is published. The year is when it became official, not when you could first use it.
- Being in the specification does not mean every browser your users run supports it. Always check.
ES2015 was also called ES6, which is why you will see "ES6" used loosely to mean "modern JavaScript".
ES2015: the big update
ES2015 changed how JavaScript is written. Most of this course uses it everywhere.
| Feature | Solves |
|---|---|
let and const | block scope instead of function-scoped var |
| Arrow functions | short callbacks and lexical this |
| Classes | clear syntax for constructors and inheritance |
| Template literals | string interpolation and multi-line strings |
| Destructuring, default parameters, rest and spread for arrays and arguments | unpacking values and flexible function signatures |
Modules (import, export) | files with their own scope and explicit dependencies |
| Promises | composable asynchronous results |
Map, Set, WeakMap, WeakSet | real collections with any key type |
Symbol, iterators, generators, for...of | custom iteration and unique keys |
Proxy and Reflect | intercepting object operations |
ES2016 to ES2019
| Edition | Feature | Example |
|---|---|---|
| ES2016 | Array.prototype.includes | tags.includes('css') |
| ES2016 | Exponentiation operator | 2 ** 10 |
| ES2017 | async and await | const data = await load() |
| ES2017 | Object.values, Object.entries | Object.entries(scores) |
| ES2017 | padStart, padEnd | '7'.padStart(3, '0') |
| ES2018 | Object rest and spread | const { id, ...rest } = course |
| ES2018 | Async iteration, for await...of | for await (const page of pages()) |
| ES2018 | Promise.prototype.finally | load().finally(hideSpinner) |
| ES2018 | Regex named groups, lookbehind, s flag | /(?<year>\d{4})/ |
| ES2019 | flat, flatMap | [[1], [2]].flat() |
| ES2019 | Object.fromEntries | Object.fromEntries(map) |
| ES2019 | trimStart, trimEnd | input.trimStart() |
| ES2019 | Optional catch binding | try { ... } catch { ... } |
ES2019 also required Array.prototype.sort to be stable, so items that compare as equal keep their original order.
ES2020
| Feature | Solves |
|---|---|
Optional chaining ?. | reading nested values that may be missing |
Nullish coalescing ?? | defaults that keep 0 and '' |
BigInt | integers larger than Number.MAX_SAFE_INTEGER |
Promise.allSettled | waiting for every promise regardless of failure |
globalThis | one name for the global object in every environment |
Dynamic import() | loading a module on demand |
String.prototype.matchAll | iterating over every regex match with groups |
import.meta | module metadata such as its URL |
const user = { name: 'Ada' };
const city = user?.address?.city ?? 'Unknown'; // → 'Unknown'
const huge = 2n ** 64n; // → 18446744073709551616n
ES2021
| Feature | Example |
|---|---|
Promise.any and AggregateError | first successful promise |
Logical assignment ||=, &&=, ??= | options.retries ??= 3 |
| Numeric separators | 1_000_000 |
String.prototype.replaceAll | slug.replaceAll(' ', '-') |
WeakRef, FinalizationRegistry | weak references for specialised caches |
ES2022
| Feature | Example |
|---|---|
| Public and private class fields, private methods | #count = 0 |
| Static class fields and static blocks | static instances = 0 |
| Private field check | #count in obj |
Top-level await in modules | const data = await fetch(url) |
Array.prototype.at | items.at(-1) for the last item |
Object.hasOwn | safer than obj.hasOwnProperty(key) |
Error cause | new Error('Load failed', { cause: error }) |
Regex match indices, d flag | start and end positions of groups |
try {
JSON.parse('{ bad json');
} catch (error) {
const wrapped = new Error('Could not read settings', { cause: error });
console.log(wrapped.cause instanceof SyntaxError); // → true
}
ES2023
| Feature | Example |
|---|---|
findLast, findLastIndex | newest matching item without reversing |
Change array by copy: toSorted, toReversed, toSpliced, with | sort without mutating |
| Hashbang comments | #!/usr/bin/env node as the first line of a script |
Symbols as WeakMap keys | non-registered symbols as weak keys |
const scores = [30, 10, 20];
const sorted = scores.toSorted((a, b) => a - b);
console.log(sorted); // → [ 10, 20, 30 ]
console.log(scores); // → [ 30, 10, 20 ] (unchanged)
ES2024
| Feature | Example |
|---|---|
Object.groupBy, Map.groupBy | group items by a key |
Promise.withResolvers | a promise plus its resolve and reject functions |
Regex v flag | set operations and better Unicode in character classes |
String.prototype.isWellFormed, toWellFormed | detect and fix lone surrogates |
Resizable ArrayBuffer and transfer | growable binary buffers |
const courses = [
{ title: 'HTML', level: 'beginner' },
{ title: 'Closures', level: 'intermediate' },
{ title: 'CSS', level: 'beginner' },
];
const byLevel = Object.groupBy(courses, (course) => course.level);
console.log(Object.keys(byLevel)); // → [ 'beginner', 'intermediate' ]
console.log(byLevel.beginner.length); // → 2
Object.groupBy runs in Node.js 21 and later, not Node.js 20, which is a good example of why checking support matters.
ES2025
| Feature | Example |
|---|---|
| Iterator helpers | iterator.filter(fn).map(fn).take(5) |
Set methods | a.union(b), a.intersection(b), a.difference(b), a.isSubsetOf(b) |
Promise.try | start a promise chain from a function that might throw synchronously |
RegExp.escape | safely insert user text into a regex |
| Import attributes and JSON modules | import data from './data.json' with { type: 'json' } |
| Regex modifiers, duplicate named groups | finer control inside patterns |
Float16Array | half-precision numbers for graphics and machine learning |
Support for the newest features is still spreading, and some are missing from older browsers and from Node.js 20. Later editions continue the pattern. TC39 publishes a list of finished proposals with the edition each one joined, and the specification itself is published at tc39.es.
Checking support
Use these, in order:
- MDN compatibility tables. Every MDN reference page ends with a table of browsers, Node.js and Deno versions. It is the quickest check.
- Baseline. web.dev and MDN label features as Baseline "newly available" when they work in all major browsers, and "widely available" once that has been true for about two and a half years. Many teams adopt "Baseline widely available" as their default policy.
- Your own targets. If you must support a particular older browser or Node.js version, test there. Node.js release notes list the V8 version, which determines its JavaScript support.
Feature detection and fallbacks
For built-in functions and methods, detect them at run time and fall back if they are missing:
function groupBy(items, getKey) {
if (typeof Object.groupBy === 'function') {
return Object.groupBy(items, getKey);
}
const groups = {};
for (const item of items) {
const key = getKey(item);
(groups[key] ??= []).push(item);
}
return groups;
}
The fallback returns an ordinary object, while Object.groupBy returns one with a null prototype. For most uses the difference does not matter.
Syntax cannot be feature-detected this way. If a browser does not understand ??= or a private field, the whole file fails to parse before any of your checks run. For syntax, either keep to what your targets support or use a build tool that transpiles newer syntax to older equivalents and adds polyfills for missing built-ins. Build tools are covered in a frontend tooling course.
Do not assume a newer feature is safe because it works in your own up-to-date browser. Many users run older devices, and company-managed machines often lag behind. Check Baseline status before shipping.
Summary
- ECMAScript editions are named by year; each contains the proposals that reached stage 4 that year.
- ES2015 introduced most of modern JavaScript; later editions add smaller, focused features.
- Recent additions such as
Object.groupBy,toSorted,Setmethods and iterator helpers replace common hand-written helpers. - Check MDN compatibility tables and Baseline status before relying on a feature.
- Feature-detect built-ins at run time; for new syntax, keep to supported features or transpile.