Modern JavaScript

Modern JavaScript (ES6+)

ES6 and the releases that followed transformed JavaScript from a quirky scripting language into a joy to write. Here are the features you will use every single day.

intermediate15 min readUpdated Sep 15, 2026
js
// cart.js
const items = [
  { name: "Keyboard", price: 80, qty: 1 },
  { name: "Mouse", price: 40, qty: 2 },
];

const total = items
  .map(({ price, qty }) => price * qty)
  .reduce((sum, n) => sum + n, 0);

console.log(`Total: $${total}`);
Released
June 2015
Also known as
ES2015 / ES6
Since then
One release every year
Biggest wins
Scope, modules, async
Key additions
Arrow, destructuring, spread
Compatibility
Universal in modern browsers

Hands-on

Try modern array methods

Change the numbers and watch the total update.

Try modern array methods

Change the numbers and watch the total update.

Why it matters

Why modern JavaScript matters

Less ceremony

Destructuring, shorthand and template literals remove boilerplate so intent stands out instead of syntax noise.

Safer scope

Block-scoped let and const replace var, eliminating a whole class of hoisting and accidental-global bugs.

Real modules

import and export gave JavaScript a standard module system, finally making code sharing and tree-shaking natural.

The big picture

Three shifts that changed everything

ES6 was not just new syntax. It changed how scope works, how code is organised and how data flows through a program.

Declarations

let, const, arrow

Block scope and concise functions changed how everyday code is written and read.

Data handling

Destructuring, spread

Pulling values apart and combining them became expressive instead of repetitive.

Composition

Modules, classes

Standard modules and class syntax made larger programs easier to organise and share.

ES6+ at a glance

The features you will reach for daily

let & const

Block-scoped bindings that make variables predictable.

Arrow functions

Short function syntax with lexical this, perfect for callbacks.

Template literals

Multi-line strings and interpolation with backticks and ${}.

Destructuring

Unpack arrays and objects into variables in a single expression.

Spread & rest

Expand values into places or collect them into one, with the same three dots.

Modules

Split code into files with import and export instead of globals.

A short history

The road from ES5 to today

  1. 2009

    ES5

    Strict mode, JSON, and array methods like map and filter become the baseline.

    09
  2. 2015

    ES6 / ES2015

    The largest update ever — let, const, arrows, classes, promises, modules and more.

    15
  3. 2017

    async / await

    Asynchronous code finally reads like synchronous code.

    17
  4. 2020

    Optional chaining

    ?. and ?? remove reams of defensive null checks.

    20
  5. Today

    Yearly releases

    TC39 ships a new, backwards-compatible edition of the standard every June.

    Today

The complete guide

Modern JavaScript (ES6+): Everything you need to know

What is ES6+?

In June 2015, JavaScript received its biggest update ever. ECMAScript 2015, better known as ES6, added block scoping, arrow functions, classes, modules, promises and much more. The committee that maintains the language then committed to a new edition every year, and “ES6+” has become shorthand for that entire modern era.

You do not need to learn every feature. A handful of them account for the vast majority of everyday code, and they are what this guide covers. The theme running through all of them is simple: say what you mean with less ceremony.

let and const

The oldest feature is also the most important. var is function-scoped and hoisted in ways that surprise people. let and const are block-scoped, so they exist only inside the nearest { }.

// scope.js
if (true) {
  let message = "inside";
  const limit = 10;
}

// message and limit are not defined out here

const prevents reassignment of the binding, which makes your intent obvious: this name will not point somewhere else. It does not freeze the value, so a const array can still be mutated. Use const by default, let when reassignment is real, and treat var as history.

Arrow functions

Arrow functions are a shorter syntax with one important behavioural difference: they do not have their own this. They inherit this from the surrounding scope, which is almost always what you want in callbacks.

// arrows.js
const add = (a, b) => a + b;

const square = (n) => {
  return n * n;
};

const names = ["Ada", "Grace", "Linus"];
const upper = names.map((name) => name.toUpperCase());

When the body is a single expression, the braces and return can be omitted. Parentheses around a single parameter are optional. Because arrow functions capture this, they are ideal for event handlers and array methods, but a poor fit for object methods that need their own this.

Template literals

Backticks create strings that span multiple lines and interpolate expressions with ${ }.

// message.js
const name = "Ada";
const role = "engineer";

const greeting = `Hello, ${name}!
You are logged in as a ${role}.`;

Template literals also support tagged templates, where a function receives the literal parts and the interpolated values separately. That mechanism underpins many templating and styling libraries.

Destructuring

Destructuring unpacks values from arrays and objects into variables, which removes a lot of repetitive property access.

// destructure.js
const user = { name: "Ada", age: 36, city: "London" };
const { name, age, city = "Unknown" } = user;

const coords = [51.5, -0.12];
const [lat, lng] = coords;

function greet({ name }) {
  return `Hello, ${name}`;
}

You can rename while destructuring (const { name: userName } = user), provide defaults, and destructure directly in a function’s parameter list — a pattern used constantly with React props and configuration objects.

Spread and rest

The same three dots do two complementary jobs depending on where they appear.

// spread.js
const a = [1, 2, 3];
const b = [4, 5];

const combined = [...a, ...b];       // spread: expand
const copy = { ...user, age: 37 };   // shallow clone + override

function sum(...numbers) {           // rest: collect
  return numbers.reduce((t, n) => t + n, 0);
}

Spread copies into a new array or object, which is the backbone of immutable updates. Rest gathers remaining arguments or elements into a single array. Together they make working with collections far more pleasant.

Default parameters and shorthand

Functions can declare default values, and object literals can use shorthand when the property name matches a variable.

// config.js
function connect({ host = "localhost", port = 8080 } = {}) {
  return `${host}:${port}`;
}

const name = "Ada";
const user = { name, greet() { return `Hi, ${this.name}`; } };

Defaults are evaluated only when the argument is undefined, which is usually what you want. Shorthand properties and methods cut noise from object-heavy code.

Optional chaining and nullish coalescing

These two operators, added in 2020, eliminated mountains of defensive checks.

// safe.js
const city = user?.address?.city;
const name = input ?? "anonymous";

user?.profile?.onLogin?.();

?. short-circuits to undefined if the value on its left is null or undefined. ?? provides a fallback only when the left side is null or undefined — unlike ||, it does not treat 0, "" or false as missing.

Array methods

The array prototype is packed with methods that describe intent instead of looping mechanics.

// arrays.js
const products = [
  { name: "Keyboard", price: 80, inStock: true },
  { name: "Mouse", price: 40, inStock: false },
  { name: "Cable", price: 10, inStock: true },
];

const names = products.map((p) => p.name);
const cheap = products.filter((p) => p.price < 50);
const total = products.reduce((sum, p) => sum + p.price, 0);
const available = products.some((p) => p.inStock);
const allCheap = products.every((p) => p.price < 100);
const mouse = products.find((p) => p.name === "Mouse");

map transforms, filter selects, reduce folds a list into one value, find returns the first match, and some/every test a condition. They chain, they do not mutate the original array, and they read like a description of what you want.

Modules

ES modules let you split code across files and share only what you choose.

// math.js
export function add(a, b) {
  return a + b;
}

export const PI = 3.14159;
export default function subtract(a, b) {
  return a - b;
}
// app.js
import subtract, { add, PI } from "./math.js";

Named exports can be several per file and are imported by name. A default export is a single primary value. Modules have their own scope, are evaluated once, and let bundlers remove unused code through tree-shaking.

Classes

Classes provide a clear syntax for object-oriented code, built on JavaScript’s existing prototype system.

// user.js
class User {
  #secret = "hidden"; // private field

  constructor(name) {
    this.name = name;
  }

  get greeting() {
    return `Hello, ${this.name}`;
  }

  static create(name) {
    return new User(name);
  }
}

class Admin extends User {
  constructor(name) {
    super(name);
    this.role = "admin";
  }
}

extends and super handle inheritance, static defines methods on the class itself, and # marks truly private fields. Under the hood it is still prototypes, but the syntax is far easier to read.

Other features worth knowing

  • Map and Set for keyed collections and unique values, with better semantics than plain objects and arrays for those jobs.
  • for…of to iterate arrays, strings, maps, sets and any iterable.
  • Exponentiation with **, so 2 ** 10 replaces Math.pow(2, 10).
  • Iterators and generators for lazy sequences, custom iteration and pausable functions.
  • Object.entries / fromEntries to convert between objects and arrays of pairs.

Best practices

  • Default to const; use let only when you reassign.
  • Prefer arrow functions for callbacks, regular functions for methods that need this.
  • Destructure in parameters to make required shape explicit.
  • Use spread for immutable updates instead of mutating shared data.
  • Reach for ?. and ?? before writing nested guards.
  • Prefer array methods over hand-written loops when transforming data.
  • Keep modules small and export the minimum surface.

Common mistakes

  • Assuming const makes an object immutable — it only prevents rebinding.
  • Using an arrow function as an object method and losing this.
  • Using || for defaults when 0 or "" are valid values; use ??.
  • Spreading only one level deep and expecting a deep clone.
  • Mixing require and import in the same module system without understanding the interop.
  • Chaining too many array methods into an unreadable pipeline.

Where to go next

Modern syntax is the vocabulary; the DOM and asynchronous JavaScript are where you put it to work. Once promises and modules feel natural, the Fetch API turns those skills into real applications.

Reaching into nested data

Optional chaining and nullish coalescing replace chains of && checks.

Prefer
const city =
  user?.address?.city ?? "Unknown";
Avoid
const city =
  user && user.address &&
  user.address.city
    ? user.address.city
    : "Unknown";

Transforming a list

map and reduce express intent and avoid manual accumulator variables.

Prefer
const names = users
  .filter((u) => u.active)
  .map((u) => u.name);
Avoid
const names = [];
for (let i = 0; i < users.length; i++) {
  if (users[i].active) {
    names.push(users[i].name);
  }
}

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning ES6+ Features?

Our interactive tutorial walks you through ES6+ Features step by step — with quizzes and real code you can run in the browser.