Frameworks

ES6 Features Every JavaScript Developer Should Know

ES6 Features Every JavaScript Developer Should Know

When ECMAScript 6 (ES2015) landed in June 2015, it was the largest single update to JavaScript since the language was created by Brendan Eich in 1995. But ES6 was not the finish line — it was the starting gun. Since 2015, the TC39 committee has shipped a new ECMAScript edition every June, steadily adding features that make JavaScript more expressive, safer, and capable. This guide covers every major feature from ES2015 through ES2025, with practical code examples for each.

ES2015 (ES6): The Foundation

ES6 introduced the features that define modern JavaScript. If you write JavaScript today, you use ES6 features in nearly every line of code.

let and const

Block-scoped variable declarations replaced the error-prone var:

// var leaks out of blocks
for (var i = 0; i < 5; i++) {}
console.log(i); // 5 — var ignores the block

// let respects block scope
for (let j = 0; j < 5; j++) {}
// console.log(j); // ReferenceError — j doesn't exist here

// const prevents reassignment
const API_URL = "https://api.example.com";
// API_URL = "other"; // TypeError

// But const objects can be mutated
const user = { name: "Alice" };
user.name = "Bob"; // This works — the reference is constant, not the value

The rule of thumb: use const by default. Switch to let only when you need to reassign. Never use var in new code.

Arrow Functions

// Concise syntax for simple operations
const double = x => x * 2;
const add = (a, b) => a + b;
const greet = name => `Hello, ${name}!`;

// Multi-line arrow functions
const processUsers = users => {
  const active = users.filter(u => u.active);
  return active.map(u => u.name);
};

// Lexical `this` — arrow functions inherit this from their parent scope
class Timer {
  constructor() {
    this.seconds = 0;
    setInterval(() => {
      this.seconds++; // `this` refers to the Timer instance
    }, 1000);
  }
}

The lexical this binding was the biggest practical improvement. Before ES6, callback functions lost their this context, requiring .bind(this) or var self = this workarounds.

Template Literals

const name = "World";
const multiLine = `
  <div class="card">
    <h3>${name}</h3>
    <p>${2 + 2} items</p>
  </div>
`;

// Tagged templates — for safe HTML, SQL, GraphQL, etc.
function html(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const val = values[i - 1];
    const escaped = String(val).replace(/&/g, '&amp;');
    return result + escaped + str;
  });
}

const userInput = '<script>alert("xss")</script>';
const safe = html`<p>${userInput}</p>`; // XSS-safe output

Destructuring

// Object destructuring
const { name, email, role = "user" } = userData;

// Array destructuring
const [first, second, ...remaining] = items;

// Nested destructuring
const { address: { city, zip } } = user;

// Parameter destructuring — common in React components
function UserCard({ name, email, avatar }) {
  return `${name} (${email})`;
}

// Swapping variables
let a = 1, b = 2;
[a, b] = [b, a]; // a=2, b=1

Spread and Rest Operators

// Spread arrays
const merged = [...arr1, ...arr2];
const copy = [...original]; // shallow clone

// Spread objects
const updated = { ...user, name: "New Name" };
const withDefaults = { theme: "light", lang: "en", ...userPrefs };

// Rest parameters (collect remaining arguments)
function log(level, ...messages) {
  messages.forEach(m => console[level](m));
}

Promises

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", `/api/users/${id}`);
    xhr.onload = () => resolve(JSON.parse(xhr.response));
    xhr.onerror = () => reject(new Error("Network error"));
    xhr.send();
  });
}

fetchUser(1)
  .then(user => fetchPosts(user.id))
  .then(posts => console.log(posts))
  .catch(err => console.error(err));

Classes, Modules, and More

// Classes
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a noise.`;
  }
}

class Dog extends Animal {
  speak() {
    return `${this.name} barks.`;
  }
}

// Modules
// math.js
export const add = (a, b) => a + b;
export default class Calculator { /* ... */ }

// app.js
import Calculator, { add } from './math.js';

// Other ES6 features worth knowing:
// Map, Set, WeakMap, WeakSet — proper data structures
// for...of — iterable protocol for arrays, strings, maps
// Symbol — unique identifiers for object properties
// Generators — function* with yield for lazy sequences
// Default parameters — function greet(name = "World")

ES2016 (ES7): Small but Important

ES2016 was deliberately small — just two features. TC39 shifted to a "train model" where features ship when they are ready rather than waiting for large batches.

// Array.prototype.includes — replaces indexOf !== -1
const fruits = ["apple", "banana", "mango"];
fruits.includes("banana"); // true
fruits.includes("grape");  // false

// Also handles NaN correctly (indexOf can't)
[1, 2, NaN].includes(NaN); // true
[1, 2, NaN].indexOf(NaN);  // -1

// Exponentiation operator
2 ** 10; // 1024 — same as Math.pow(2, 10)
const area = side ** 2;

ES2017 (ES8): async/await

The biggest feature since ES6 itself. async/await makes asynchronous code read like synchronous code:

// Before — promise chains
function loadDashboard() {
  return fetchUser()
    .then(user => fetchPosts(user.id))
    .then(posts => fetchComments(posts[0].id))
    .then(comments => renderDashboard(comments))
    .catch(handleError);
}

// After — async/await
async function loadDashboard() {
  try {
    const user = await fetchUser();
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    renderDashboard(comments);
  } catch (error) {
    handleError(error);
  }
}

// Parallel execution with Promise.all
async function loadAll() {
  const [users, posts, settings] = await Promise.all([
    fetch("/api/users").then(r => r.json()),
    fetch("/api/posts").then(r => r.json()),
    fetch("/api/settings").then(r => r.json()),
  ]);
  return { users, posts, settings };
}

Other ES2017 additions: Object.values(), Object.entries(), String.prototype.padStart()/padEnd(), and trailing commas in function parameter lists.

ES2018 (ES9): Rest/Spread for Objects and Async Iteration

// Object rest/spread (previously only arrays)
const { id, ...rest } = user; // rest has everything except id
const merged = { ...defaults, ...overrides };

// Async iteration — for...await...of
async function processStream(stream) {
  for await (const chunk of stream) {
    console.log(chunk);
  }
}

// Named capture groups in regex
const datePattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = "2025-03-15".match(datePattern);
console.log(match.groups.year);  // "2025"
console.log(match.groups.month); // "03"

// Promise.prototype.finally
fetch("/api/data")
  .then(r => r.json())
  .catch(err => console.error(err))
  .finally(() => hideLoadingSpinner());

ES2019 (ES10): Practical Utilities

// Array.prototype.flat and flatMap
const nested = [[1, 2], [3, [4, 5]]];
nested.flat();    // [1, 2, 3, [4, 5]]
nested.flat(2);   // [1, 2, 3, 4, 5]
nested.flat(Infinity); // flatten everything

const sentences = ["Hello world", "Goodbye moon"];
sentences.flatMap(s => s.split(" ")); // ["Hello", "world", "Goodbye", "moon"]

// Object.fromEntries — inverse of Object.entries
const entries = [["name", "Alice"], ["age", 30]];
const obj = Object.fromEntries(entries); // { name: "Alice", age: 30 }

// Useful for transforming objects
const prices = { apple: 1.2, banana: 0.5, mango: 2.0 };
const doubled = Object.fromEntries(
  Object.entries(prices).map(([key, val]) => [key, val * 2])
);

// Optional catch binding
try {
  JSON.parse(input);
} catch {
  // no need to declare the error parameter if you don't use it
  return fallbackValue;
}

// String.prototype.trimStart() and trimEnd()
"  hello  ".trimStart(); // "hello  "
"  hello  ".trimEnd();   // "  hello"

ES2020 (ES11): Nullish Coalescing and Optional Chaining

Two of the most practical features JavaScript has ever received:

// Optional chaining — safe property access
const city = user?.address?.city; // undefined if any part is null/undefined
const first = arr?.[0];           // safe array access
const result = obj?.method?.();   // safe method call

// Nullish coalescing — default only for null/undefined
const port = config.port ?? 3000;
// Unlike ||, this doesn't treat 0, "", or false as "missing"
const count = data.count ?? 10; // 0 stays as 0
const name = data.name || "Anonymous"; // "" becomes "Anonymous"
const name2 = data.name ?? "Anonymous"; // "" stays as ""

// BigInt — arbitrary precision integers
const huge = 9007199254740991n; // Beyond Number.MAX_SAFE_INTEGER
const result = huge + 1n;       // 9007199254740992n

// Promise.allSettled — wait for all, even failures
const results = await Promise.allSettled([
  fetch("/api/users"),
  fetch("/api/posts"),
  fetch("/api/comments"),
]);
// results: [{ status: "fulfilled", value: ... }, { status: "rejected", reason: ... }, ...]

// globalThis — universal global object
// Works in browsers (window), Node (global), workers (self)
globalThis.setTimeout(() => {}, 100);

// Dynamic import
const module = await import(`./routes/${routeName}.js`);

ES2021 (ES12): String Methods and Logical Assignment

// String.prototype.replaceAll
const slug = "Hello World Foo".replaceAll(" ", "-").toLowerCase();
// "hello-world-foo"

// Logical assignment operators
user.name ??= "Anonymous";  // assign only if null/undefined
config.debug ||= false;     // assign only if falsy
cache.data &&= transform(cache.data); // assign only if truthy

// Promise.any — resolves with first fulfilled promise
const fastest = await Promise.any([
  fetch("https://cdn1.example.com/data"),
  fetch("https://cdn2.example.com/data"),
  fetch("https://cdn3.example.com/data"),
]);

// WeakRef and FinalizationRegistry (advanced memory management)
const weakRef = new WeakRef(largeObject);
const deref = weakRef.deref(); // object or undefined if garbage collected

// Numeric separators — readability for large numbers
const billion = 1_000_000_000;
const hex = 0xFF_EC_D5_9C;
const binary = 0b1010_0001;

ES2022 (ES13): Class Fields and Top-Level Await

// Class fields — no more constructor boilerplate
class User {
  // Public fields
  name = "Anonymous";
  posts = [];

  // Private fields (cannot be accessed outside the class)
  #password;
  #loginAttempts = 0;

  constructor(name, password) {
    this.name = name;
    this.#password = password;
  }

  #validatePassword(input) {
    this.#loginAttempts++;
    return input === this.#password;
  }

  // Static fields and methods
  static DEFAULT_ROLE = "user";
  static create(name) {
    return new User(name, crypto.randomUUID());
  }
}

// Top-level await — use await outside async functions in modules
const config = await fetch("/config.json").then(r => r.json());
const db = await connectToDatabase(config.dbUrl);

// Array.prototype.at — access from the end with negative indices
const arr = [10, 20, 30, 40, 50];
arr.at(0);   // 10
arr.at(-1);  // 50
arr.at(-2);  // 40

// Object.hasOwn — safer replacement for hasOwnProperty
Object.hasOwn(user, "name"); // true
// Unlike user.hasOwnProperty("name"), this works
// even if hasOwnProperty is overridden

// Error cause — chain errors with context
try {
  await db.query(sql);
} catch (err) {
  throw new Error("Failed to load user data", { cause: err });
}

ES2023 (ES14): Array Search from End and Immutable Sorting

// findLast and findLastIndex — search from the end
const transactions = [
  { amount: 100, type: "credit" },
  { amount: 50, type: "debit" },
  { amount: 200, type: "credit" },
];
const lastCredit = transactions.findLast(t => t.type === "credit");
// { amount: 200, type: "credit" }

// Immutable array methods — return new arrays instead of mutating
const nums = [3, 1, 4, 1, 5];
const sorted = nums.toSorted();      // [1, 1, 3, 4, 5] — nums unchanged
const reversed = nums.toReversed();  // [5, 1, 4, 1, 3] — nums unchanged
const spliced = nums.toSpliced(2, 1, 99); // [3, 1, 99, 1, 5]
const changed = nums.with(0, 100);   // [100, 1, 4, 1, 5]

// Hashbang grammar — #!/usr/bin/env node at file start
// Now officially part of the spec for CLI scripts

// WeakMap keys can now be Symbols (not just Objects)
const metadata = new WeakMap();
const key = Symbol("id");
metadata.set(key, { created: Date.now() });

ES2024 (ES15): Grouping, Promise.withResolvers, and More

// Object.groupBy and Map.groupBy
const people = [
  { name: "Alice", dept: "Engineering" },
  { name: "Bob", dept: "Marketing" },
  { name: "Charlie", dept: "Engineering" },
];

const byDept = Object.groupBy(people, p => p.dept);
// { Engineering: [Alice, Charlie], Marketing: [Bob] }

// Promise.withResolvers — cleaner deferred promises
const { promise, resolve, reject } = Promise.withResolvers();
// No need for the executor function pattern
setTimeout(() => resolve("done"), 1000);

// Atomics.waitAsync — non-blocking atomic waits for SharedArrayBuffer
// Used in multi-threaded WebAssembly and worker scenarios

// Regular expression v flag — set notation and properties of strings
const emoji = /[\p{Emoji_Presentation}]/v;
emoji.test("😀"); // true

// String.prototype.isWellFormed and toWellFormed
const str = "Hello \uD800 World"; // lone surrogate
str.isWellFormed(); // false
str.toWellFormed(); // "Hello � World"

ES2025 (ES16): What Just Landed

// Set methods — union, intersection, difference, and more
const frontend = new Set(["Alice", "Bob", "Charlie"]);
const backend = new Set(["Bob", "Diana", "Eve"]);

frontend.union(backend);
// Set {"Alice", "Bob", "Charlie", "Diana", "Eve"}

frontend.intersection(backend);
// Set {"Bob"}

frontend.difference(backend);
// Set {"Alice", "Charlie"}

frontend.symmetricDifference(backend);
// Set {"Alice", "Charlie", "Diana", "Eve"}

frontend.isSubsetOf(backend); // false
frontend.isSupersetOf(new Set(["Alice"])); // true

// Iterator helpers — chainable operations on any iterator
function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const first10Even = fibonacci()
  .filter(n => n % 2 === 0)
  .take(10)
  .toArray();

// Import attributes (formerly import assertions)
import data from "./config.json" with { type: "json" };
import styles from "./theme.css" with { type: "css" };

// RegExp duplicate named capture groups
const dateRegex = /(?<value>\d{4})-\d{2}|(?<value>\d{2}\/\d{2})/v;
// Same name "value" in different alternatives — now allowed

How Modern Frameworks Use These Features

Understanding ECMAScript evolution is not academic — every modern framework builds on these primitives. React hooks rely on closures and destructuring. Vue 3's Composition API uses Proxies (ES2015) extensively. Svelte's reactivity compiles down to signals using fine-grained updates.

When comparing React, Vue, and Svelte, the syntax differences often come down to how each framework wraps standard JavaScript features. Knowing the raw language features helps you understand what the framework magic is actually doing.

Most modern projects rely on build tools and transpilers configured through your code editor to handle compatibility. Tools like Babel, esbuild, and SWC let you write ES2025 code while shipping bundles that work in older environments. Performance optimization often starts with understanding which features your target browsers support natively versus what needs polyfilling.

A Practical Compatibility Guide

Not every feature is safe to use everywhere without transpilation. Here is a practical breakdown:

Safe everywhere (ES2015-ES2020): let/const, arrow functions, template literals, destructuring, spread, promises, async/await, optional chaining, nullish coalescing. All modern browsers and Node.js 14+ support these natively.

Safe in modern targets (ES2021-ES2023): replaceAll, logical assignment, class fields, private fields, at(), structuredClone, toSorted/toReversed. Supported in browsers from 2022+ and Node.js 18+.

Requires latest engines (ES2024-ES2025): Object.groupBy, Set methods, iterator helpers, import attributes. Supported in browsers from late 2024+ and Node.js 22+. Use with a build tool if you need wider support.

Frequently Asked Questions

Do I need to learn every ES version?

No. Focus on ES2015-ES2020 features first — they are the most impactful and most commonly used. Once those are second nature, learn newer features as you encounter them in code reviews or documentation. Many ES2023-2025 features are conveniences that simplify existing patterns rather than introducing entirely new concepts.

Should I still use Babel in 2025?

For most projects targeting modern browsers, no. Native browser support for ES2015-2022 features is universal. If you need to support older environments, esbuild or SWC are faster alternatives to Babel. If you are building a library that must run in Node.js 14 or IE11-era browsers, Babel still has the widest transformation support.

What is the difference between ES6 and ES2015?

They are the same thing. "ES6" is the informal name (sixth edition), while "ES2015" is the official name (year of publication). After ES6, the committee switched to year-based naming exclusively. There is no "ES7" officially — it is ES2016. But you will still see "ES6" used widely because it was such a landmark release.

How do I check if my code uses features my target browsers support?

Use browserslist configuration (shared by Babel, esbuild, and PostCSS) to define your target browsers. The eslint-plugin-compat plugin will warn you when you use unsupported APIs. The caniuse.com database tracks feature support across all browsers. For Node.js, the node.green website shows feature support per version.