JavaScript ES6+ Features: The Complete Modern JavaScript Guide

February 11, 2026 · 35 min read

JavaScript has changed more in the last decade than in the previous twenty years combined. When ES6 (ECMAScript 2015) landed, it was the biggest update the language had ever received. Arrow functions, classes, modules, promises, destructuring, template literals, and dozens of other features transformed JavaScript from a language with well-known quirks into a modern, powerful programming language capable of building anything from server-side APIs to complex single-page applications.

But ES6 was just the beginning. Every year since 2015, the TC39 committee has shipped new features: async/await in ES2017, optional chaining in ES2020, private class fields in ES2022, and more. Collectively, these are called ES6+ features, and they define how JavaScript is written in 2026.

This guide covers every major ES6+ feature you need to know. Each section includes practical code examples, ES5 comparisons where relevant, and guidance on when and how to use each feature. Whether you are upgrading legacy code or learning modern JavaScript from scratch, this is your complete reference.

⚙ Try it live: Test any code example in this guide with our JavaScript Runner or the full JS Playground.

Table of Contents

  1. let and const
  2. Arrow Functions
  3. Template Literals
  4. Destructuring
  5. Spread and Rest Operators
  6. Default Parameters
  7. Enhanced Object Literals
  8. Classes
  9. Modules
  10. Promises and Async/Await
  11. Map, Set, WeakMap, WeakSet
  12. Symbols and Iterators
  13. Generators
  14. Optional Chaining and Nullish Coalescing
  15. Modern Array Methods
  16. Modern Object Methods
  17. Frequently Asked Questions

1. let and const

Before ES6, JavaScript had only one way to declare variables: var. The problem with var is that it is function-scoped, not block-scoped, and it is hoisted to the top of its function. This led to an entire category of subtle bugs that plagued JavaScript developers for years.

ES6 introduced let and const, which are block-scoped. They exist only within the nearest pair of curly braces, whether that is an if statement, a for loop, or any other block.

Block Scoping

// ES5 — var is function-scoped
function es5Example() {
  if (true) {
    var x = 10;
  }
  console.log(x); // 10 — x leaks out of the if block
}

// ES6 — let and const are block-scoped
function es6Example() {
  if (true) {
    let y = 10;
    const z = 20;
  }
  console.log(y); // ReferenceError: y is not defined
  console.log(z); // ReferenceError: z is not defined
}

// This matters most in loops
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 — var shares one i across all iterations

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 100);
}
// Prints: 0, 1, 2 — let creates a new j for each iteration

The Temporal Dead Zone

Variables declared with let and const exist in a temporal dead zone (TDZ) from the start of their block until the line where they are declared. Accessing them in the TDZ throws a ReferenceError. This catches bugs that var's hoisting would silently hide.

// var is hoisted — no error, but value is undefined
console.log(a); // undefined
var a = 5;

// let is in the TDZ — throws an error
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 5;

// const behaves the same way
console.log(c); // ReferenceError
const c = 5;

const Does Not Mean Immutable

const means the binding cannot be reassigned. The value itself can still be mutated if it is an object or array.

const user = { name: 'Alice', age: 30 };
user.age = 31;           // OK — mutating the object
user.email = 'a@b.com';  // OK — adding a property
user = { name: 'Bob' };  // TypeError: Assignment to constant variable

const numbers = [1, 2, 3];
numbers.push(4);         // OK — mutating the array
numbers[0] = 99;         // OK
numbers = [5, 6, 7];     // TypeError

// To make an object truly immutable, use Object.freeze()
const frozen = Object.freeze({ name: 'Alice', age: 30 });
frozen.age = 31;         // Silently fails (throws in strict mode)
console.log(frozen.age); // 30

Best Practice

Use const by default. Switch to let only when you need to reassign. Never use var. This convention makes your code easier to reason about because most variables should not change after initialization.

2. Arrow Functions

Arrow functions provide a shorter syntax for writing functions and, more importantly, do not bind their own this. They inherit this from the enclosing lexical scope, which eliminates one of the most confusing aspects of JavaScript.

Syntax Variations

// ES5 function
const add = function(a, b) {
  return a + b;
};

// ES6 arrow function
const add = (a, b) => {
  return a + b;
};

// Implicit return (single expression)
const add = (a, b) => a + b;

// Single parameter — parentheses optional
const double = n => n * 2;

// No parameters — parentheses required
const greet = () => 'Hello!';

// Returning an object literal — wrap in parentheses
const makeUser = (name, age) => ({ name, age });

// Multiline with explicit return
const processData = (data) => {
  const cleaned = data.trim();
  const parsed = JSON.parse(cleaned);
  return parsed;
};

Lexical this Binding

The most important difference between arrow functions and regular functions is how they handle this. Arrow functions capture this from their surrounding scope at the time they are defined.

// ES5 — the "this" problem
function Timer() {
  this.seconds = 0;
  setInterval(function() {
    this.seconds++;        // BUG: "this" is the global object, not Timer
    console.log(this.seconds); // NaN
  }, 1000);
}

// ES5 workaround — save "this" in a variable
function Timer() {
  var self = this;
  self.seconds = 0;
  setInterval(function() {
    self.seconds++;        // Works, but ugly
  }, 1000);
}

// ES6 — arrow functions inherit "this"
function Timer() {
  this.seconds = 0;
  setInterval(() => {
    this.seconds++;        // "this" is the Timer instance
    console.log(this.seconds);
  }, 1000);
}

When NOT to Use Arrow Functions

// 1. Object methods — arrow functions get the wrong "this"
const counter = {
  count: 0,
  increment: () => {
    this.count++;  // BUG: "this" is not the counter object
  }
};

// Use a regular method instead
const counter = {
  count: 0,
  increment() {
    this.count++;  // Correct: "this" is the counter object
  }
};

// 2. Constructors — arrow functions cannot be used with "new"
const Person = (name) => {
  this.name = name;
};
new Person('Alice'); // TypeError: Person is not a constructor

// 3. When you need "arguments" object
const fn = () => {
  console.log(arguments); // ReferenceError in strict mode
};

// Use rest parameters instead
const fn = (...args) => {
  console.log(args); // Works perfectly
};

// 4. Event handlers that need "this" to be the element
button.addEventListener('click', () => {
  this.classList.toggle('active'); // BUG: "this" is not the button
});

button.addEventListener('click', function() {
  this.classList.toggle('active'); // Correct
});

3. Template Literals

Template literals use backticks instead of quotes and support string interpolation, multiline strings, and tagged templates. They eliminate the need for string concatenation and make complex string building readable.

String Interpolation

const name = 'Alice';
const age = 30;

// ES5 — string concatenation
var greeting = 'Hello, ' + name + '! You are ' + age + ' years old.';

// ES6 — template literal
const greeting = `Hello, ${name}! You are ${age} years old.`;

// Expressions inside ${}
const price = 9.99;
const quantity = 3;
const total = `Total: $${(price * quantity).toFixed(2)}`;
// "Total: $29.97"

// Function calls
const upper = `Name: ${name.toUpperCase()}`;

// Ternary expressions
const status = `User is ${age >= 18 ? 'an adult' : 'a minor'}`;

Multiline Strings

// ES5 — multiline was painful
var html = '
\n' + '

' + title + '

\n' + '

' + description + '

\n' + '
'; // ES6 — natural multiline const html = `

${title}

${description}

`; // SQL queries become readable const query = ` SELECT users.name, orders.total FROM users JOIN orders ON users.id = orders.user_id WHERE orders.total > ${minAmount} ORDER BY orders.total DESC LIMIT ${limit} `;

Tagged Templates

Tagged templates let you parse template literals with a function. The tag function receives the string parts and interpolated values separately, enabling powerful string processing patterns.

// A tag function receives strings array and values
function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = values[i] !== undefined ? `${values[i]}` : '';
    return result + str + value;
  }, '');
}

const name = 'Alice';
const role = 'admin';
const html = highlight`User ${name} has role ${role}`;
// "User Alice has role admin"

// Practical: SQL injection prevention
function sql(strings, ...values) {
  return {
    text: strings.join('$'),
    values: values
  };
}

const userId = 42;
const query = sql`SELECT * FROM users WHERE id = ${userId}`;
// { text: "SELECT * FROM users WHERE id = $", values: [42] }

// Practical: CSS-in-JS (used by styled-components)
function css(strings, ...values) {
  return strings.reduce((result, str, i) =>
    result + str + (values[i] || ''), ''
  );
}

const color = '#3b82f6';
const styles = css`
  background: ${color};
  padding: 1rem;
  border-radius: 8px;
`;

4. Destructuring

Destructuring lets you extract values from arrays and properties from objects into distinct variables. It is one of the most-used ES6 features and makes working with data structures dramatically more concise.

Object Destructuring

const user = { name: 'Alice', age: 30, email: 'alice@example.com' };

// ES5
var name = user.name;
var age = user.age;
var email = user.email;

// ES6 — object destructuring
const { name, age, email } = user;

// Rename variables
const { name: userName, age: userAge } = user;
console.log(userName); // 'Alice'

// Default values
const { name, age, role = 'user' } = user;
console.log(role); // 'user' — property doesn't exist, default used

// Rename with default
const { name: n, role: r = 'viewer' } = user;

// Skip properties you don't need
const { email } = user; // only extract email

// In function parameters — extremely common
function greet({ name, age }) {
  return `Hello ${name}, you are ${age}`;
}
greet(user); // "Hello Alice, you are 30"

// With defaults in parameters
function createUser({ name, role = 'user', active = true } = {}) {
  return { name, role, active };
}
createUser({ name: 'Bob' }); // { name: 'Bob', role: 'user', active: true }

Array Destructuring

const rgb = [255, 128, 0];

// ES5
var red = rgb[0];
var green = rgb[1];
var blue = rgb[2];

// ES6 — array destructuring
const [red, green, blue] = rgb;

// Skip elements
const [first, , third] = [1, 2, 3];
console.log(first, third); // 1, 3

// Default values
const [a = 0, b = 0, c = 0, d = 0] = [1, 2];
console.log(d); // 0

// Swap variables without a temp variable
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2, 1

// From function returns
function getCoordinates() {
  return [40.7128, -74.0060];
}
const [lat, lng] = getCoordinates();

// With rest operator
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]

Nested Destructuring

// Nested objects
const config = {
  server: {
    host: 'localhost',
    port: 3000,
    ssl: { enabled: true, cert: '/path/to/cert' }
  },
  database: {
    url: 'mongodb://localhost:27017',
    name: 'myapp'
  }
};

const {
  server: { host, port, ssl: { enabled: sslEnabled } },
  database: { url: dbUrl }
} = config;

console.log(host);       // 'localhost'
console.log(sslEnabled); // true
console.log(dbUrl);      // 'mongodb://localhost:27017'

// Nested arrays
const matrix = [[1, 2], [3, 4], [5, 6]];
const [[a, b], [c, d]] = matrix;
console.log(a, b, c, d); // 1, 2, 3, 4

// Mixed nesting
const response = {
  data: {
    users: [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ]
  }
};

const { data: { users: [firstUser] } } = response;
console.log(firstUser); // { id: 1, name: 'Alice' }

5. Spread and Rest Operators

The ... syntax serves two purposes depending on context. As the spread operator, it expands iterables into individual elements. As the rest operator, it collects multiple elements into an array or object.

Spread for Arrays

// Clone an array (shallow copy)
const original = [1, 2, 3];
const clone = [...original];

// ES5 equivalent
var clone = original.slice();

// Merge arrays
const frontend = ['React', 'Vue', 'Svelte'];
const backend = ['Node', 'Deno', 'Bun'];
const fullstack = [...frontend, ...backend];
// ['React', 'Vue', 'Svelte', 'Node', 'Deno', 'Bun']

// Insert elements in the middle
const arr = [1, 2, 5, 6];
const expanded = [...arr.slice(0, 2), 3, 4, ...arr.slice(2)];
// [1, 2, 3, 4, 5, 6]

// Convert iterable to array
const chars = [... 'hello']; // ['h', 'e', 'l', 'l', 'o']
const unique = [...new Set([1, 1, 2, 3, 3])]; // [1, 2, 3]

// Pass array as function arguments
const numbers = [5, 2, 8, 1, 9];
const max = Math.max(...numbers); // 9

// ES5 equivalent
var max = Math.max.apply(null, numbers);

Spread for Objects

// Clone an object (shallow copy)
const user = { name: 'Alice', age: 30 };
const clone = { ...user };

// Merge objects (later properties overwrite earlier ones)
const defaults = { theme: 'dark', lang: 'en', notifications: true };
const userPrefs = { theme: 'light', lang: 'fr' };
const settings = { ...defaults, ...userPrefs };
// { theme: 'light', lang: 'fr', notifications: true }

// Add or override properties
const updated = { ...user, age: 31, email: 'alice@example.com' };
// { name: 'Alice', age: 31, email: 'alice@example.com' }

// Conditionally add properties
const config = {
  host: 'localhost',
  port: 3000,
  ...(process.env.NODE_ENV === 'production' && { ssl: true }),
  ...(apiKey && { apiKey })
};

// Immutable state update (common in React)
const state = { count: 0, items: ['a', 'b'] };
const newState = {
  ...state,
  count: state.count + 1,
  items: [...state.items, 'c']
};

Rest in Functions

// Rest parameters collect remaining arguments into an array
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4, 5); // 15

// ES5 equivalent — using the arguments object
function sum() {
  return Array.prototype.slice.call(arguments)
    .reduce(function(total, n) { return total + n; }, 0);
}

// Rest after named parameters
function log(level, ...messages) {
  console.log(`[${level}]`, ...messages);
}
log('ERROR', 'Connection failed', 'Retrying in 5s');
// [ERROR] Connection failed Retrying in 5s

Rest in Destructuring

// Object rest — collect remaining properties
const { name, age, ...rest } = {
  name: 'Alice', age: 30, email: 'a@b.com', role: 'admin'
};
console.log(rest); // { email: 'a@b.com', role: 'admin' }

// Great for removing properties immutably
const { password, ...safeUser } = userWithPassword;
// safeUser has everything except password

// Array rest — collect remaining elements
const [first, second, ...remaining] = [1, 2, 3, 4, 5];
console.log(remaining); // [3, 4, 5]

6. Default Parameters

Default parameters let you set fallback values for function arguments. They replace the clunky ES5 pattern of checking for undefined at the top of every function.

// ES5 — manual defaults
function createUser(name, role, active) {
  name = name || 'Anonymous';
  role = role || 'user';
  active = active !== undefined ? active : true;
  return { name, role, active };
}
// BUG: createUser('', 'admin', false) — empty string and false are falsy!

// ES6 — default parameters
function createUser(name = 'Anonymous', role = 'user', active = true) {
  return { name, role, active };
}

createUser();                          // { name: 'Anonymous', role: 'user', active: true }
createUser('Alice');                   // { name: 'Alice', role: 'user', active: true }
createUser('Bob', 'admin');            // { name: 'Bob', role: 'admin', active: true }
createUser('Charlie', 'user', false);  // { name: 'Charlie', role: 'user', active: false }
// Correctly handles empty string and false!

// Defaults can be expressions
function getTimestamp(date = new Date()) {
  return date.toISOString();
}

// Defaults can reference earlier parameters
function createElement(tag = 'div', className = `${tag}-default`) {
  return { tag, className };
}
createElement();        // { tag: 'div', className: 'div-default' }
createElement('span');  // { tag: 'span', className: 'span-default' }

// Combined with destructuring
function fetchData({
  url,
  method = 'GET',
  headers = {},
  timeout = 5000,
  retry = 3
} = {}) {
  console.log(`${method} ${url} (timeout: ${timeout}ms, retries: ${retry})`);
}

fetchData({ url: '/api/users' });
// GET /api/users (timeout: 5000ms, retries: 3)

7. Enhanced Object Literals

ES6 enhanced object literals with shorthand properties, shorthand methods, and computed property names. These reduce boilerplate when creating objects.

Shorthand Properties

const name = 'Alice';
const age = 30;
const email = 'alice@example.com';

// ES5
var user = {
  name: name,
  age: age,
  email: email
};

// ES6 — when property name matches variable name
const user = { name, age, email };
// { name: 'Alice', age: 30, email: 'alice@example.com' }

// Mix shorthand and regular properties
const config = {
  host,
  port,
  ssl: true,
  maxRetries: 3
};

Shorthand Methods

// ES5
var calculator = {
  add: function(a, b) {
    return a + b;
  },
  subtract: function(a, b) {
    return a - b;
  }
};

// ES6 — shorthand method syntax
const calculator = {
  add(a, b) {
    return a + b;
  },
  subtract(a, b) {
    return a - b;
  }
};

// Works with generators and async too
const obj = {
  *generator() {
    yield 1;
    yield 2;
  },
  async fetchData() {
    const res = await fetch('/api/data');
    return res.json();
  }
};

Computed Property Names

// ES5 — had to create object, then add dynamic keys
var obj = {};
obj[dynamicKey] = value;

// ES6 — computed property names inline
const key = 'color';
const obj = {
  [key]: 'blue',
  [`${key}Hex`]: '#0000ff'
};
// { color: 'blue', colorHex: '#0000ff' }

// Practical: dynamic state updates
function updateField(state, field, value) {
  return { ...state, [field]: value };
}

const state = { name: 'Alice', age: 30 };
updateField(state, 'age', 31);
// { name: 'Alice', age: 31 }

// Practical: creating enum-like objects
const ACTIONS = ['CREATE', 'READ', 'UPDATE', 'DELETE'];
const handlers = Object.fromEntries(
  ACTIONS.map(action => [action, () => console.log(`Handling ${action}`)])
);

8. Classes

ES6 classes provide a cleaner syntax for creating objects and implementing inheritance. Under the hood, they still use JavaScript's prototype-based inheritance, but the syntax is far more readable than the ES5 constructor function pattern.

Basic Class Syntax

// ES5 — constructor function pattern
function Animal(name, sound) {
  this.name = name;
  this.sound = sound;
}
Animal.prototype.speak = function() {
  return this.name + ' says ' + this.sound;
};

// ES6 — class syntax
class Animal {
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
  }

  speak() {
    return `${this.name} says ${this.sound}`;
  }

  toString() {
    return `[Animal: ${this.name}]`;
  }
}

const dog = new Animal('Rex', 'woof');
dog.speak(); // "Rex says woof"

Getters and Setters

class Temperature {
  #celsius; // private field

  constructor(celsius) {
    this.#celsius = celsius;
  }

  get fahrenheit() {
    return this.#celsius * 9 / 5 + 32;
  }

  set fahrenheit(f) {
    this.#celsius = (f - 32) * 5 / 9;
  }

  get celsius() {
    return this.#celsius;
  }

  set celsius(c) {
    if (c < -273.15) throw new RangeError('Below absolute zero');
    this.#celsius = c;
  }
}

const temp = new Temperature(100);
console.log(temp.fahrenheit); // 212
temp.fahrenheit = 32;
console.log(temp.celsius);    // 0

Static Methods and Properties

class MathUtils {
  static PI = 3.14159265359;

  static square(n) {
    return n * n;
  }

  static cube(n) {
    return n ** 3;
  }

  static clamp(value, min, max) {
    return Math.min(Math.max(value, min), max);
  }
}

// Called on the class, not instances
MathUtils.square(5);      // 25
MathUtils.PI;             // 3.14159265359
MathUtils.clamp(15, 0, 10); // 10

// Static methods are often used as factory methods
class User {
  constructor(name, email, role) {
    this.name = name;
    this.email = email;
    this.role = role;
  }

  static fromJSON(json) {
    const data = JSON.parse(json);
    return new User(data.name, data.email, data.role);
  }

  static guest() {
    return new User('Guest', '', 'viewer');
  }
}

const user = User.fromJSON('{"name":"Alice","email":"a@b.com","role":"admin"}');
const guest = User.guest();

Inheritance with extends

class Shape {
  constructor(color = 'black') {
    this.color = color;
  }

  describe() {
    return `A ${this.color} shape`;
  }
}

class Circle extends Shape {
  constructor(radius, color) {
    super(color);          // Must call super() before using "this"
    this.radius = radius;
  }

  get area() {
    return Math.PI * this.radius ** 2;
  }

  describe() {
    return `A ${this.color} circle with radius ${this.radius}`;
  }
}

class Rectangle extends Shape {
  constructor(width, height, color) {
    super(color);
    this.width = width;
    this.height = height;
  }

  get area() {
    return this.width * this.height;
  }
}

const circle = new Circle(5, 'red');
circle.describe(); // "A red circle with radius 5"
circle.area;       // 78.539...
circle instanceof Circle; // true
circle instanceof Shape;  // true

Private Fields and Methods (ES2022)

class BankAccount {
  #balance;        // private field
  #owner;

  constructor(owner, initialBalance = 0) {
    this.#owner = owner;
    this.#balance = initialBalance;
  }

  get balance() {
    return this.#balance;
  }

  deposit(amount) {
    this.#validateAmount(amount);
    this.#balance += amount;
    return this;
  }

  withdraw(amount) {
    this.#validateAmount(amount);
    if (amount > this.#balance) {
      throw new Error('Insufficient funds');
    }
    this.#balance -= amount;
    return this;
  }

  #validateAmount(amount) {   // private method
    if (amount <= 0) throw new Error('Amount must be positive');
    if (!Number.isFinite(amount)) throw new Error('Invalid amount');
  }

  toString() {
    return `${this.#owner}'s account: $${this.#balance.toFixed(2)}`;
  }
}

const account = new BankAccount('Alice', 1000);
account.deposit(500).withdraw(200);
console.log(account.balance);  // 1300
// account.#balance;           // SyntaxError: Private field
// account.#validateAmount(5); // SyntaxError: Private method

9. Modules

ES modules (import/export) are JavaScript's native module system. They replace CommonJS (require/module.exports) in the browser and are increasingly used in Node.js as well. Modules give you file-level scope, explicit dependencies, and enable tree-shaking in bundlers.

Named Exports

// math.js — named exports
export const PI = 3.14159;

export function square(n) {
  return n * n;
}

export function cube(n) {
  return n ** 3;
}

export class Vector {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

// Alternative: export at the bottom
const E = 2.71828;
function log(n) { return Math.log(n); }
export { E, log };

Named Imports

// Import specific exports
import { PI, square, cube } from './math.js';
console.log(square(5)); // 25

// Rename on import
import { PI as pi, square as sq } from './math.js';

// Import everything as a namespace
import * as MathUtils from './math.js';
console.log(MathUtils.PI);        // 3.14159
console.log(MathUtils.square(5)); // 25

Default Exports

// logger.js — default export (one per module)
export default class Logger {
  constructor(prefix) {
    this.prefix = prefix;
  }

  log(message) {
    console.log(`[${this.prefix}] ${message}`);
  }

  error(message) {
    console.error(`[${this.prefix} ERROR] ${message}`);
  }
}

// Import default — no curly braces, name is your choice
import Logger from './logger.js';
import MyLogger from './logger.js'; // same thing, different name

// Mix default and named imports
import Logger, { formatMessage, LogLevel } from './logger.js';

Re-exports

// index.js — barrel file pattern
export { default as Logger } from './logger.js';
export { square, cube, PI } from './math.js';
export { default as HttpClient } from './http.js';

// Consumers import from one place
import { Logger, square, HttpClient } from './utils/index.js';

Dynamic Imports

// Dynamic import returns a Promise — great for code splitting
async function loadChart() {
  const { Chart } = await import('./chart.js');
  const chart = new Chart('#container');
  chart.render(data);
}

// Conditional loading
if (user.role === 'admin') {
  const { AdminPanel } = await import('./admin.js');
  AdminPanel.init();
}

// Lazy loading routes (common in SPA frameworks)
const routes = {
  '/dashboard': () => import('./pages/dashboard.js'),
  '/settings': () => import('./pages/settings.js'),
  '/profile': () => import('./pages/profile.js')
};

async function navigate(path) {
  const module = await routes[path]();
  module.default.render();
}

10. Promises and Async/Await

Promises and async/await are the foundation of asynchronous JavaScript. Promises replaced callback-based patterns in ES6, and async/await (ES2017) made working with promises as straightforward as writing synchronous code.

Creating Promises

// A Promise wraps an asynchronous operation
const fetchUser = new Promise((resolve, reject) => {
  setTimeout(() => {
    const user = { id: 1, name: 'Alice' };
    if (user) {
      resolve(user);  // success
    } else {
      reject(new Error('User not found'));  // failure
    }
  }, 1000);
});

// Consuming a Promise
fetchUser
  .then(user => console.log(user.name))  // "Alice"
  .catch(err => console.error(err.message));

// Shorthand for resolved/rejected promises
const resolved = Promise.resolve(42);
const rejected = Promise.reject(new Error('Failed'));

Promise Chaining

// ES5 — callback hell
getUser(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      console.log(details);
    }, handleError);
  }, handleError);
}, handleError);

// ES6 — Promise chain (flat and readable)
getUser(userId)
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetails(orders[0].id))
  .then(details => console.log(details))
  .catch(err => console.error(err)); // catches any error in the chain

// Each .then() receives the return value of the previous one
fetch('/api/users/1')
  .then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  })
  .then(user => {
    console.log(user.name);
    return fetch(`/api/users/${user.id}/orders`);
  })
  .then(response => response.json())
  .then(orders => console.log(`${orders.length} orders`))
  .catch(err => console.error('Request failed:', err))
  .finally(() => console.log('Done')); // runs whether success or failure

Async/Await

// async/await makes Promises look synchronous
async function getUserOrders(userId) {
  try {
    const userRes = await fetch(`/api/users/${userId}`);
    if (!userRes.ok) throw new Error(`HTTP ${userRes.status}`);

    const user = await userRes.json();

    const ordersRes = await fetch(`/api/users/${user.id}/orders`);
    const orders = await ordersRes.json();

    return { user, orders };
  } catch (err) {
    console.error('Failed to fetch:', err.message);
    throw err;  // re-throw if caller should handle it
  } finally {
    console.log('Request completed');
  }
}

// async functions always return a Promise
const result = getUserOrders(1);
result.then(data => console.log(data));

// Or await it from another async function
async function main() {
  const { user, orders } = await getUserOrders(1);
  console.log(`${user.name} has ${orders.length} orders`);
}

// Top-level await (ES2022, in modules)
const config = await fetch('/config.json').then(r => r.json());

Promise.all, Promise.race, Promise.allSettled, Promise.any

// Promise.all — waits for ALL promises, fails if ANY fails
const [users, posts, comments] = await Promise.all([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json())
]);
// All three requests run in parallel

// Promise.race — resolves/rejects with the FIRST settled promise
const result = await Promise.race([
  fetch('/api/data'),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), 5000)
  )
]);
// Either gets data or times out after 5 seconds

// Promise.allSettled — waits for ALL, never rejects
const results = await Promise.allSettled([
  fetch('/api/users'),
  fetch('/api/broken-endpoint'),
  fetch('/api/posts')
]);

results.forEach((result, i) => {
  if (result.status === 'fulfilled') {
    console.log(`Request ${i}: success`);
  } else {
    console.log(`Request ${i}: failed — ${result.reason.message}`);
  }
});
// No try/catch needed — allSettled never rejects

// Promise.any — resolves with the FIRST fulfilled promise
// Rejects only if ALL promises reject
const fastest = await Promise.any([
  fetch('https://cdn1.example.com/data.json'),
  fetch('https://cdn2.example.com/data.json'),
  fetch('https://cdn3.example.com/data.json')
]);
// Uses whichever CDN responds first

Error Handling Patterns

// Pattern 1: try/catch in async functions
async function fetchData(url) {
  try {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('Request was cancelled');
    } else {
      console.error('Fetch failed:', err.message);
    }
    return null;
  }
}

// Pattern 2: Go-style error handling (no try/catch)
async function to(promise) {
  try {
    const result = await promise;
    return [null, result];
  } catch (err) {
    return [err, null];
  }
}

const [err, user] = await to(fetchUser(1));
if (err) {
  console.error('Failed:', err.message);
  return;
}
console.log(user.name);

// Pattern 3: Retry with exponential backoff
async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.json();
    } catch (err) {
      if (i === retries - 1) throw err;
      const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

11. Map, Set, WeakMap, WeakSet

ES6 introduced four new collection types that address limitations of plain objects and arrays. Map and Set are the ones you will use most often.

Map

A Map holds key-value pairs where keys can be any type, not just strings. It preserves insertion order and has a size property.

// Creating a Map
const userRoles = new Map();
userRoles.set('alice', 'admin');
userRoles.set('bob', 'editor');
userRoles.set('charlie', 'viewer');

// Or initialize with entries
const userRoles = new Map([
  ['alice', 'admin'],
  ['bob', 'editor'],
  ['charlie', 'viewer']
]);

// Get, has, delete
userRoles.get('alice');    // 'admin'
userRoles.has('bob');      // true
userRoles.delete('charlie');
userRoles.size;            // 2

// Keys can be ANY type — not just strings
const objKey = { id: 1 };
const fnKey = () => {};
const map = new Map();
map.set(objKey, 'object value');
map.set(fnKey, 'function value');
map.set(42, 'number value');

// Iteration
for (const [key, value] of userRoles) {
  console.log(`${key}: ${value}`);
}

userRoles.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});

// Convert to/from objects and arrays
const obj = Object.fromEntries(userRoles);     // Map to Object
const map2 = new Map(Object.entries(obj));      // Object to Map
const arr = [...userRoles];                     // Map to Array

// Map vs Object: when to use each
// Use Map when:
//   - Keys are not strings (objects, numbers, etc.)
//   - You need to know the size
//   - You iterate frequently
//   - Keys are added/removed often (Map is optimized for this)
// Use Object when:
//   - Keys are strings
//   - You need JSON serialization
//   - You need prototype chain or methods

Set

A Set stores unique values of any type. It is the best way to deduplicate data and check membership efficiently.

// Creating a Set
const tags = new Set(['javascript', 'es6', 'web']);
tags.add('typescript');
tags.add('javascript'); // Ignored — already exists

tags.has('es6');    // true
tags.delete('web');
tags.size;          // 3

// Deduplicate an array
const numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
const unique = [...new Set(numbers)];
// [1, 2, 3, 4]

// Set operations (built-in since ES2025, or implement manually)
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);

// Union
const union = new Set([...a, ...b]);
// Set {1, 2, 3, 4, 5, 6}

// Intersection
const intersection = new Set([...a].filter(x => b.has(x)));
// Set {3, 4}

// Difference (in a but not in b)
const difference = new Set([...a].filter(x => !b.has(x)));
// Set {1, 2}

// Practical: track unique visitors
const visitors = new Set();
function recordVisit(userId) {
  visitors.add(userId);
  return visitors.size; // unique visitor count
}

// Practical: fast membership testing
const validCodes = new Set(['US', 'CA', 'UK', 'DE', 'FR', 'JP']);
function isValidCountry(code) {
  return validCodes.has(code); // O(1) lookup
}

WeakMap and WeakSet

WeakMap and WeakSet hold weak references to their keys (WeakMap) or values (WeakSet). Objects in them can be garbage collected if there are no other references. They are not iterable and have no size property.

// WeakMap — associate private data with objects
const privateData = new WeakMap();

class User {
  constructor(name, password) {
    this.name = name;
    privateData.set(this, { password }); // private data stored externally
  }

  checkPassword(input) {
    return privateData.get(this).password === input;
  }
}

const user = new User('Alice', 'secret123');
user.checkPassword('secret123'); // true
// When "user" is garbage collected, the WeakMap entry is too

// WeakMap for caching expensive computations
const cache = new WeakMap();

function expensiveComputation(obj) {
  if (cache.has(obj)) return cache.get(obj);
  const result = /* heavy calculation */ obj.value * 2;
  cache.set(obj, result);
  return result;
}

// WeakSet — track objects without preventing garbage collection
const processed = new WeakSet();

function processNode(node) {
  if (processed.has(node)) return; // skip if already processed
  processed.add(node);
  // ... process the DOM node
}
// When nodes are removed from DOM and garbage collected,
// they are automatically removed from the WeakSet

12. Symbols and Iterators

Symbols are a new primitive type introduced in ES6. Each symbol is unique, making them perfect for creating non-conflicting property keys. The iterator protocol, powered by the well-known Symbol.iterator, is what makes for...of loops, spread syntax, and destructuring work.

Symbols

// Every symbol is unique
const s1 = Symbol('description');
const s2 = Symbol('description');
console.log(s1 === s2); // false — always unique

// Use as object property keys — no name collisions
const ID = Symbol('id');
const user = {
  [ID]: 12345,
  name: 'Alice'
};

user[ID]; // 12345
// Symbol properties don't show up in for...in or Object.keys()
Object.keys(user);                // ['name']
Object.getOwnPropertySymbols(user); // [Symbol(id)]

// Practical: prevent accidental property overwriting
const INTERNAL_STATE = Symbol('internal');
class Plugin {
  constructor() {
    this[INTERNAL_STATE] = { initialized: false };
  }
}
// Third-party code can't accidentally overwrite INTERNAL_STATE
// because they don't have a reference to that specific symbol

// Global symbols — shared across the entire runtime
const globalSym = Symbol.for('app.config');
const sameSym = Symbol.for('app.config');
console.log(globalSym === sameSym); // true
Symbol.keyFor(globalSym);           // 'app.config'

The Iterator Protocol

// An iterable is any object with a [Symbol.iterator]() method
// that returns an iterator (object with a next() method)

// Built-in iterables: String, Array, Map, Set, arguments, NodeList

// Custom iterable: a Range class
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { done: true };
      }
    };
  }
}

const range = new Range(1, 5);
for (const n of range) {
  console.log(n); // 1, 2, 3, 4, 5
}

// Works with spread, destructuring, Array.from
const nums = [...new Range(1, 5)];        // [1, 2, 3, 4, 5]
const [first, second] = new Range(10, 20); // 10, 11
Array.from(new Range(1, 3));               // [1, 2, 3]

// Making a plain object iterable
const colorPalette = {
  colors: ['#ff0000', '#00ff00', '#0000ff'],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.colors.length) {
          return { value: this.colors[index++], done: false };
        }
        return { done: true };
      }
    };
  }
};

for (const color of colorPalette) {
  console.log(color); // '#ff0000', '#00ff00', '#0000ff'
}

Well-Known Symbols

// Symbol.toPrimitive — control type conversion
class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number': return this.amount;
      case 'string': return `${this.amount} ${this.currency}`;
      default: return this.amount;
    }
  }
}

const price = new Money(42.99, 'USD');
+price;           // 42.99
`${price}`;       // "42.99 USD"
price + 10;       // 52.99

// Symbol.hasInstance — customize instanceof
class EvenNumber {
  static [Symbol.hasInstance](num) {
    return typeof num === 'number' && num % 2 === 0;
  }
}

4 instanceof EvenNumber;  // true
3 instanceof EvenNumber;  // false

13. Generators

Generators are functions that can be paused and resumed. They are declared with function* and use the yield keyword to produce a sequence of values lazily, one at a time.

Basic Generators

// Generator function — note the asterisk
function* countUp(start = 1) {
  let n = start;
  while (true) {
    yield n++;
  }
}

const counter = countUp();
counter.next(); // { value: 1, done: false }
counter.next(); // { value: 2, done: false }
counter.next(); // { value: 3, done: false }
// Runs forever, but lazily — only computes when asked

// Finite generator
function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

// Take first 10 Fibonacci numbers
function take(n, iterable) {
  const result = [];
  for (const value of iterable) {
    result.push(value);
    if (result.length === n) break;
  }
  return result;
}

take(10, fibonacci());
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

yield and Two-Way Communication

// yield can receive values from next()
function* conversation() {
  const name = yield 'What is your name?';
  const age = yield `Hello ${name}! How old are you?`;
  return `${name} is ${age} years old`;
}

const chat = conversation();
chat.next();        // { value: "What is your name?", done: false }
chat.next('Alice'); // { value: "Hello Alice! How old are you?", done: false }
chat.next(30);      // { value: "Alice is 30 years old", done: true }

// Practical: stateful ID generator
function* idGenerator(prefix = 'id') {
  let id = 0;
  while (true) {
    yield `${prefix}_${++id}`;
  }
}

const userId = idGenerator('user');
userId.next().value; // 'user_1'
userId.next().value; // 'user_2'
userId.next().value; // 'user_3'

Delegating with yield*

// yield* delegates to another iterable or generator
function* flatten(arr) {
  for (const item of arr) {
    if (Array.isArray(item)) {
      yield* flatten(item);  // recursively delegate
    } else {
      yield item;
    }
  }
}

const nested = [1, [2, [3, [4]], 5], 6];
const flat = [...flatten(nested)];
// [1, 2, 3, 4, 5, 6]

// Compose generators
function* range(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* sequence() {
  yield* range(1, 3);   // 1, 2, 3
  yield* range(10, 12); // 10, 11, 12
  yield 99;             // 99
}

[...sequence()]; // [1, 2, 3, 10, 11, 12, 99]

Practical: Paginated API Fetching

// Generator that handles pagination automatically
async function* fetchPages(baseUrl) {
  let page = 1;
  while (true) {
    const res = await fetch(`${baseUrl}?page=${page}`);
    const data = await res.json();

    if (data.results.length === 0) return;

    yield* data.results;  // yield each item from the page
    page++;
  }
}

// Consume lazily — only fetches pages as needed
for await (const user of fetchPages('/api/users')) {
  console.log(user.name);
  if (someCondition) break; // stops fetching more pages
}

14. Optional Chaining (?.) and Nullish Coalescing (??)

These two operators, introduced in ES2020, eliminate the most common sources of TypeError: Cannot read property of undefined errors and fix a long-standing problem with using || for default values.

Optional Chaining (?.)

// The problem: accessing deeply nested properties
const user = {
  name: 'Alice',
  address: {
    street: '123 Main St'
    // city is undefined
  }
  // settings is undefined
};

// ES5 — defensive coding (verbose and error-prone)
var city = user && user.address && user.address.city;
var theme = user && user.settings && user.settings.theme;

// ES2020 — optional chaining
const city = user?.address?.city;     // undefined (no error)
const theme = user?.settings?.theme;  // undefined (no error)

// Works with method calls
const result = user?.getProfile?.();  // undefined if getProfile doesn't exist

// Works with bracket notation
const key = 'address';
const addr = user?.[key]?.street;     // '123 Main St'

// Works with arrays
const firstOrder = user?.orders?.[0]; // undefined if orders is null/undefined

// Real-world example: API response handling
const data = await fetch('/api/user/1').then(r => r.json());
const avatarUrl = data?.profile?.avatar?.url ?? '/default-avatar.png';
const tags = data?.metadata?.tags?.join(', ') ?? 'no tags';

// DOM traversal
const value = document.querySelector('#email')?.value;
const text = element?.firstChild?.textContent;

Nullish Coalescing (??)

// The problem with || for defaults
const port = config.port || 3000;
// BUG: if config.port is 0, it's falsy, so you get 3000

const name = input.name || 'Anonymous';
// BUG: if input.name is '', it's falsy, so you get 'Anonymous'

const debug = options.debug || true;
// BUG: if options.debug is false, it's falsy, so you get true

// ES2020 — ?? only falls through for null and undefined
const port = config.port ?? 3000;
// config.port = 0 => 0 (correct!)
// config.port = undefined => 3000

const name = input.name ?? 'Anonymous';
// input.name = '' => '' (correct!)
// input.name = undefined => 'Anonymous'

const debug = options.debug ?? true;
// options.debug = false => false (correct!)
// options.debug = undefined => true

// Combine with optional chaining
const theme = user?.settings?.theme ?? 'dark';
const fontSize = config?.display?.fontSize ?? 14;
const locale = navigator?.language ?? 'en-US';

// Nullish coalescing assignment (??=)
const options = {};
options.timeout ??= 5000;  // Only assigns if null/undefined
options.retries ??= 3;
// { timeout: 5000, retries: 3 }

// Does NOT overwrite falsy values like 0 or ''
const settings = { volume: 0, name: '' };
settings.volume ??= 50;  // 0 (not overwritten)
settings.name ??= 'default'; // '' (not overwritten)

15. Modern Array Methods

JavaScript continues to add useful array methods. These additions from ES2019 through ES2023 solve common patterns that previously required manual implementation.

⚙ Deep dive: For a comprehensive reference of every array method, see our JavaScript Array Methods: The Complete Guide.

flat() and flatMap() (ES2019)

// flat() — flatten nested arrays
const nested = [1, [2, 3], [4, [5, 6]]];
nested.flat();     // [1, 2, 3, 4, [5, 6]] — depth 1 (default)
nested.flat(2);    // [1, 2, 3, 4, 5, 6]
nested.flat(Infinity); // flattens all levels

// flatMap() — map then flatten one level (more efficient than map + flat)
const sentences = ['Hello world', 'Good morning'];
sentences.flatMap(s => s.split(' '));
// ['Hello', 'world', 'Good', 'morning']

// One-to-many transformations
const users = [
  { name: 'Alice', roles: ['admin', 'editor'] },
  { name: 'Bob', roles: ['viewer'] }
];
users.flatMap(u => u.roles);
// ['admin', 'editor', 'viewer']

// Filter and transform in one pass
const nums = [1, 2, 3, 4, 5];
nums.flatMap(n => n % 2 === 0 ? [n * 10] : []);
// [20, 40]

at() (ES2022)

// at() supports negative indexing — no more arr[arr.length - 1]
const arr = ['a', 'b', 'c', 'd', 'e'];

arr.at(0);   // 'a'
arr.at(2);   // 'c'
arr.at(-1);  // 'e' — last element
arr.at(-2);  // 'd' — second to last

// ES5 equivalent for negative index
arr[arr.length - 1]; // 'e'

// Works on strings too
'hello'.at(-1); // 'o'

// Practical: get the last element of a chain
const last = results.sort((a, b) => a.score - b.score).at(-1);

findLast() and findLastIndex() (ES2023)

const transactions = [
  { id: 1, type: 'credit', amount: 100 },
  { id: 2, type: 'debit', amount: 50 },
  { id: 3, type: 'credit', amount: 200 },
  { id: 4, type: 'debit', amount: 75 }
];

// find() returns the FIRST match
transactions.find(t => t.type === 'credit');
// { id: 1, type: 'credit', amount: 100 }

// findLast() returns the LAST match
transactions.findLast(t => t.type === 'credit');
// { id: 3, type: 'credit', amount: 200 }

// findLastIndex() returns the index of the last match
transactions.findLastIndex(t => t.type === 'debit');
// 3

Array.from() with Mapping

// Create arrays from iterables, array-like objects, or generators
Array.from('hello');              // ['h', 'e', 'l', 'l', 'o']
Array.from(new Set([1, 1, 2]));   // [1, 2]
Array.from(document.querySelectorAll('div')); // real array from NodeList

// Second argument is a mapping function
Array.from({ length: 5 }, (_, i) => i * 2);
// [0, 2, 4, 6, 8]

// Generate a range
Array.from({ length: 10 }, (_, i) => i + 1);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// Initialize a 2D grid
Array.from({ length: 3 }, () => Array.from({ length: 3 }, () => 0));
// [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

Immutable Array Methods (ES2023)

const arr = [3, 1, 4, 1, 5];

// toSorted() — sort without mutating
const sorted = arr.toSorted((a, b) => a - b);
// sorted: [1, 1, 3, 4, 5], arr: unchanged

// toReversed() — reverse without mutating
const reversed = arr.toReversed();
// reversed: [5, 1, 4, 1, 3], arr: unchanged

// toSpliced() — splice without mutating
const spliced = arr.toSpliced(1, 2, 'a', 'b');
// spliced: [3, 'a', 'b', 1, 5], arr: unchanged

// with() — replace one element without mutating
const replaced = arr.with(0, 99);
// replaced: [99, 1, 4, 1, 5], arr: unchanged

// These are ideal for React/Redux state management
// where immutability matters

16. Modern Object Methods

Objects have gained powerful new static methods that simplify common operations like converting between objects and arrays, merging data, and checking properties safely.

Object.entries() and Object.fromEntries()

// Object.entries() — convert object to [key, value] pairs
const user = { name: 'Alice', age: 30, role: 'admin' };
Object.entries(user);
// [['name', 'Alice'], ['age', 30], ['role', 'admin']]

// Iterate over object properties
for (const [key, value] of Object.entries(user)) {
  console.log(`${key}: ${value}`);
}

// Transform object values
const prices = { apple: 1.5, banana: 0.75, cherry: 2.0 };
const discounted = Object.fromEntries(
  Object.entries(prices).map(([fruit, price]) => [fruit, price * 0.9])
);
// { apple: 1.35, banana: 0.675, cherry: 1.8 }

// Filter object properties
const filtered = Object.fromEntries(
  Object.entries(prices).filter(([_, price]) => price > 1)
);
// { apple: 1.5, cherry: 2.0 }

// Convert Map to Object
const map = new Map([['a', 1], ['b', 2]]);
const obj = Object.fromEntries(map);
// { a: 1, b: 2 }

// Convert URLSearchParams to Object
const params = new URLSearchParams('name=Alice&age=30&role=admin');
const queryObj = Object.fromEntries(params);
// { name: 'Alice', age: '30', role: 'admin' }

Object.keys() and Object.values()

const config = { host: 'localhost', port: 3000, debug: true };

Object.keys(config);   // ['host', 'port', 'debug']
Object.values(config); // ['localhost', 3000, true]

// Check if object is empty
const isEmpty = Object.keys(obj).length === 0;

// Count properties
const propCount = Object.keys(config).length; // 3

// Get all values for processing
const allValues = Object.values(config);
const hasDebug = Object.values(config).includes(true);

Object.hasOwn() (ES2022)

// Object.hasOwn() replaces obj.hasOwnProperty()
const user = { name: 'Alice', age: 30 };

// ES5 — problematic if object overrides hasOwnProperty
user.hasOwnProperty('name');  // true, but fragile

// Safer ES5 pattern
Object.prototype.hasOwnProperty.call(user, 'name'); // true, but verbose

// ES2022 — clean and safe
Object.hasOwn(user, 'name');  // true
Object.hasOwn(user, 'email'); // false

// Works with objects that don't have Object.prototype
const bare = Object.create(null);
bare.key = 'value';
// bare.hasOwnProperty('key'); // TypeError!
Object.hasOwn(bare, 'key');    // true — works perfectly

// Practical: safely check for optional properties
function processConfig(config) {
  const port = Object.hasOwn(config, 'port') ? config.port : 3000;
  const host = Object.hasOwn(config, 'host') ? config.host : 'localhost';
  return { host, port };
}

Object.assign() and Structured Clone

// Object.assign() — shallow merge/clone
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
Object.assign(target, source);
// target is now { a: 1, b: 3, c: 4 }

// Clone (prefer spread syntax)
const clone = Object.assign({}, original);
const clone2 = { ...original }; // cleaner

// structuredClone() — deep clone (ES2022)
const original = {
  name: 'Alice',
  scores: [90, 85, 92],
  address: { city: 'NYC', zip: '10001' },
  createdAt: new Date()
};

const deep = structuredClone(original);
deep.scores.push(100);
deep.address.city = 'LA';
console.log(original.scores);      // [90, 85, 92] — unchanged
console.log(original.address.city); // 'NYC' — unchanged

// structuredClone handles: Date, RegExp, Map, Set, ArrayBuffer, etc.
// Does NOT handle: functions, DOM nodes, symbols

Frequently Asked Questions

What is the difference between var, let, and const in JavaScript?

var is function-scoped and hoisted, which can lead to confusing bugs. let and const are block-scoped (limited to the nearest curly braces) and exist in a temporal dead zone before their declaration. Use const for values that will not be reassigned (most variables), and let when you need to reassign. Avoid var in modern JavaScript.

When should I use arrow functions vs regular functions?

Use arrow functions for callbacks, array methods (map, filter, reduce), and short inline functions. Use regular functions for object methods, constructors, and when you need your own this binding. Arrow functions inherit this from their surrounding scope, which makes them unsuitable for methods that need to reference their own object.

What is the difference between Promises and async/await?

Promises and async/await solve the same problem: handling asynchronous operations. async/await is syntactic sugar built on top of Promises. It makes asynchronous code look and behave like synchronous code, which is easier to read and debug. Use async/await for most new code, but understand Promises because async/await uses them under the hood and methods like Promise.all() are still essential.

Should I use ES6 classes or plain objects in JavaScript?

Use classes when you need inheritance hierarchies, when working with frameworks that expect them (like React class components or Angular), or when modeling entities with shared behavior. Use plain objects and functions (composition) when you need flexibility, when mixing behaviors from multiple sources, or for simple data containers. Many modern patterns favor composition over inheritance.

What are JavaScript modules and why should I use them?

JavaScript modules (import/export) let you split code into separate files, each with its own scope. This prevents global variable pollution, makes dependencies explicit, enables tree-shaking (removing unused code), and makes large codebases manageable. ES modules are the standard module system supported natively in all modern browsers and Node.js.

Is ES6 supported in all browsers in 2026?

Yes. All modern browsers (Chrome, Firefox, Safari, Edge) fully support ES6 and most ES2015-ES2024 features. You no longer need Babel to transpile ES6 for browser compatibility. However, some cutting-edge proposals (stage 3 or below) may need transpilation. For Node.js, version 18 and later supports virtually all modern JavaScript features including ES modules natively.

Conclusion

ES6 and its successors transformed JavaScript from a language with well-known rough edges into a modern, expressive, and powerful language. The features covered in this guide are not theoretical or niche — they are the foundation of how professional JavaScript is written in 2026. Every framework, library, and codebase you encounter uses these patterns daily.

If you are coming from ES5, focus on the features you will use most often: const/let, arrow functions, template literals, destructuring, spread/rest, and async/await. These six features alone will transform how you write code. Once they feel natural, explore classes, modules, generators, and the newer additions like optional chaining and nullish coalescing.

If you are already comfortable with ES6 basics, pay attention to the newer features: private class fields, structuredClone, Object.hasOwn(), immutable array methods (toSorted, toReversed, toSpliced, with), and Promise.allSettled(). These are the patterns that distinguish a modern codebase from one that merely uses ES6 syntax.

The best way to learn these features is to use them. Refactor a function to use destructuring. Replace a callback chain with async/await. Convert a constructor function to a class. Each small change builds muscle memory, and before long, modern JavaScript will feel like the only natural way to write code.

⚙ Practice: Try every example in this guide using our JavaScript Runner, or explore the full JS Playground for a complete coding environment.

Learn More

Related: Check out our JavaScript Array Methods Complete Guide for an exhaustive reference on array operations, and our JavaScript Runner to experiment with the code examples above.

Related Resources

JavaScript Array Methods Guide
Complete reference for every JavaScript array method
JavaScript Runner
Run and test JavaScript code instantly
JS Playground
Full-featured JavaScript coding environment
JavaScript Array Methods Cheat Sheet
All array methods at a glance
TypeScript Types Cheat Sheet
Type-safe JavaScript reference
TypeScript Tips and Tricks
Advanced TypeScript patterns