JavaScript ES6+ Features Cheatsheet
JavaScript has evolved dramatically since ES6 (ECMAScript 2015) was released. Every year brings new features that make the language more expressive, concise, and powerful. Yet many developers still write ES5-style code out of habit, missing out on tools that could make their work dramatically cleaner. This cheatsheet covers every major ES6+ feature you need to know — not just the syntax, but when and why to use each one — so you can write truly modern JavaScript.
ES6+ features are not just syntactic sugar. They fundamentally change how you structure programs — enabling immutable data patterns, cleaner async code, tree-shaking-friendly module systems, and prototype-free OOP. All modern browsers and Node.js 14+ support every feature covered here with zero transpilation needed.
Variables: let, const, and Why You Should Ditch var
The most fundamental ES6 change is the introduction of block-scoped variables. Understanding why this matters requires understanding JavaScript's function-scoped var and the problems it caused.
The Problem with var
// var is hoisted and function-scoped — causes bugs
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3 (not 0, 1, 2!)
// let is block-scoped — behaves as expected
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2
// const: use for values that should never be reassigned
const API_URL = 'https://api.example.com';
const config = { timeout: 5000, retries: 3 };
// const prevents reassignment but NOT mutation
config.timeout = 10000; // This works
// config = {}; // TypeError: Assignment to constant variable
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | Yes (initialized as undefined) | Yes (TDZ — not accessible) | Yes (TDZ — not accessible) |
| Re-declarable | Yes | No | No |
| Re-assignable | Yes | Yes | No |
| Use today? | No | When value changes | Default choice |
Rule of thumb: Default to const. Only use let when you know the variable will be reassigned (loop counters, accumulator variables). Never use var.
Arrow Functions: Concise Syntax and Lexical this
Arrow functions are one of the most-used ES6 features — and the most misunderstood. They are not just a shorter function syntax. They capture this from the enclosing scope (lexical this), which eliminates an entire class of bugs that plagued pre-ES6 JavaScript.
// Traditional function
const double = function(n) { return n * 2; };
// Arrow function — equivalent
const double = (n) => n * 2;
// Multiple params — parentheses required
const add = (a, b) => a + b;
// No params — empty parens required
const getTimestamp = () => Date.now();
// Multi-line body — curly braces + explicit return
const processUser = (user) => {
const name = user.name.trim();
return { ...user, name };
};
// Returning an object literal — wrap in parens to avoid ambiguity
const toPoint = (x, y) => ({ x, y });
// Lexical 'this' — the key difference
class Timer {
constructor() {
this.seconds = 0;
}
start() {
// Arrow function captures 'this' from start() method
setInterval(() => {
this.seconds++; // 'this' correctly refers to Timer instance
console.log(this.seconds);
}, 1000);
}
startBroken() {
// Regular function — 'this' is undefined in strict mode
setInterval(function() {
this.seconds++; // TypeError: Cannot read properties of undefined
}, 1000);
}
}
Avoid arrow functions as object methods (const obj = { greet: () => this.name }) — this will not refer to the object. Also avoid them as constructors or when you need the arguments object. For event handlers where you need this to refer to the element, use regular functions.
Destructuring: Unpacking Arrays and Objects
Destructuring lets you extract values from arrays and objects into distinct variables in a single statement. It's not just about brevity — it makes your intent explicit and enables powerful patterns like swapping variables and partial parameter extraction.
// === OBJECT DESTRUCTURING ===
const user = { id: 1, name: 'Alice', role: 'admin', age: 30 };
// Basic extraction
const { name, role } = user;
console.log(name); // 'Alice'
// Rename while destructuring
const { name: userName, role: userRole } = user;
console.log(userName); // 'Alice'
// Default values
const { name, department = 'Engineering' } = user;
console.log(department); // 'Engineering' (not in user object)
// Nested destructuring
const response = {
status: 200,
data: { user: { id: 42, email: 'alice@example.com' } }
};
const { data: { user: { email } } } = response;
console.log(email); // 'alice@example.com'
// Rest in objects
const { id, ...rest } = user;
console.log(rest); // { name: 'Alice', role: 'admin', age: 30 }
// === ARRAY DESTRUCTURING ===
const colors = ['red', 'green', 'blue', 'yellow'];
// Basic extraction
const [first, second] = colors;
console.log(first); // 'red'
console.log(second); // 'green'
// Skip elements with commas
const [, , third] = colors;
console.log(third); // 'blue'
// Rest in arrays
const [head, ...tail] = colors;
console.log(tail); // ['green', 'blue', 'yellow']
// Swap variables — no temp variable needed
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2, 1
// Destructuring function return values
function getMinMax(arr) {
return [Math.min(...arr), Math.max(...arr)];
}
const [min, max] = getMinMax([3, 1, 4, 1, 5, 9, 2]);
// === FUNCTION PARAMETER DESTRUCTURING ===
// Instead of: function render(options) { const title = options.title; ... }
function render({ title, subtitle = 'No subtitle', theme = 'light' }) {
console.log(title, subtitle, theme);
}
render({ title: 'Hello', theme: 'dark' });
// 'Hello', 'No subtitle', 'dark'
Spread and Rest: The Three Dots
The ... syntax serves two purposes depending on context: spread expands an iterable into individual elements; rest collects multiple elements into an array. Together they enable immutable data manipulation, flexible function signatures, and clean array/object composition.
// === SPREAD OPERATOR ===
// Clone arrays (shallow)
const original = [1, 2, 3];
const clone = [...original];
clone.push(4); // original is unchanged
// Merge arrays
const nums = [1, 2, 3];
const moreNums = [4, 5, 6];
const all = [...nums, ...moreNums, 7, 8]; // [1,2,3,4,5,6,7,8]
// Clone objects (shallow)
const user = { name: 'Alice', role: 'admin' };
const userClone = { ...user };
// Merge/override objects (later keys win)
const defaults = { theme: 'light', lang: 'en', fontSize: 14 };
const userPrefs = { theme: 'dark', fontSize: 16 };
const config = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'en', fontSize: 16 }
// Immutable state updates (React pattern)
const state = { count: 0, user: null, loading: false };
const newState = { ...state, count: state.count + 1, loading: true };
// Spread into function call
function sum(a, b, c) { return a + b + c; }
const args = [1, 2, 3];
console.log(sum(...args)); // 6
// === REST PARAMETERS ===
// Collect remaining arguments
function log(level, ...messages) {
console.log(`[${level}]`, messages.join(' '));
}
log('INFO', 'Server', 'started', 'on', 'port', '3000');
// [INFO] Server started on port 3000
// Better than arguments object — is a real array
function sum(...nums) {
return nums.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3, 4, 5); // 15
ES6+ Feature Landscape — all of these ship natively in modern browsers
Template Literals and Tagged Templates
Template literals replace string concatenation with a far more readable syntax. But their real power lies in tagged templates — a lesser-known feature used by libraries like styled-components, GraphQL (gql), and SQL query builders to safely interpolate dynamic values.
const name = 'Alice';
const role = 'admin';
// ES5 — painful concatenation
const msg = 'Hello, ' + name + '! You are logged in as ' + role + '.';
// ES6 template literal — clean and readable
const msg = `Hello, ${name}! You are logged in as ${role}.`;
// Multi-line strings — no \n needed
const html = `
<div class="user-card">
<h2>${name}</h2>
<span>${role}</span>
</div>
`;
// Expressions inside ${}
const price = 29.99;
const tax = 0.08;
const receipt = `
Subtotal: $${price.toFixed(2)}
Tax: $${(price * tax).toFixed(2)}
Total: $${(price * (1 + tax)).toFixed(2)}
`;
// Tagged templates — advanced use
function highlight(strings, ...values) {
return strings.reduce((acc, str, i) => {
const val = values[i] !== undefined
? `<mark>${values[i]}</mark>`
: '';
return acc + str + val;
}, '');
}
const searchTerm = 'JavaScript';
const result = highlight`Learn ${searchTerm} in 2026`;
// 'Learn <mark>JavaScript</mark> in 2026'
// Safe SQL queries with tagged templates (prevents injection)
function sql(strings, ...values) {
const query = strings.join('?');
return { query, params: values };
}
const userId = 42;
const { query, params } = sql`SELECT * FROM users WHERE id = ${userId}`;
Promises and Async/Await
Asynchronous programming is at the heart of JavaScript. ES6 introduced Promises to replace callback hell, and ES2017's async/await syntax makes async code look and behave like synchronous code — while maintaining non-blocking execution under the hood.
// === PROMISES ===
// Creating a Promise
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: 'Alice' });
} else {
reject(new Error('Invalid user ID'));
}
}, 100);
});
}
// Consuming — .then() / .catch()
fetchUser(1)
.then(user => console.log(user.name))
.catch(err => console.error(err.message))
.finally(() => console.log('Done'));
// Promise.all — run in parallel, wait for all
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
// Promise.allSettled — wait for all, don't fail fast
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(-1), // will reject
]);
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.error(r.reason);
});
// Promise.race — first one wins
const fastest = await Promise.race([fetch('/api/a'), fetch('/api/b')]);
// === ASYNC / AWAIT ===
// async function always returns a Promise
async function loadDashboard(userId) {
try {
// await pauses execution until Promise resolves
const user = await fetchUser(userId);
const [posts, notifications] = await Promise.all([
fetchPosts(user.id),
fetchNotifications(user.id),
]);
return { user, posts, notifications };
} catch (error) {
// Errors from rejected Promises land here
console.error('Failed to load dashboard:', error.message);
throw error; // re-throw if you want caller to handle
}
}
// Top-level await (ES2022, works in modules)
const data = await loadDashboard(1);
console.log(data.user.name);
ES6 Modules: import and export
ES modules are the official, browser-native module system. Unlike CommonJS (require), ES modules are statically analyzed, which enables tree-shaking — build tools can eliminate unused exports, reducing bundle size. Every serious modern project uses ES modules.
// === EXPORTING ===
// math.js — named exports (multiple per file)
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
// Can also export at the bottom
const subtract = (a, b) => a - b;
export { subtract };
// Default export (one per file — usually the main thing)
// user.js
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() { return `Hi, I'm ${this.name}`; }
}
// Re-exporting (barrel files / index.js pattern)
// utils/index.js
export { add, multiply, subtract } from './math.js';
export { default as User } from './user.js';
// === IMPORTING ===
// Named imports — must match export name
import { add, multiply } from './math.js';
// Rename on import
import { add as sum, multiply as product } from './math.js';
// Default import — you choose the name
import User from './user.js';
// Import all named exports as namespace
import * as MathUtils from './math.js';
MathUtils.add(2, 3); // 5
// Import default AND named together
import User, { PI } from './utils/index.js';
// Dynamic import — loads module on demand (code splitting)
const { default: Chart } = await import('./chart.js');
const chart = new Chart(canvas);
// Conditional dynamic import
if (userPreference === 'dark') {
const { darkTheme } = await import('./themes.js');
applyTheme(darkTheme);
}
Classes, Inheritance, and Private Fields
ES6 classes provide a clean syntax over JavaScript's prototype-based inheritance. ES2022 added true private fields with # prefix — no more convention-based privacy (_field). Understanding how classes map to prototypes helps you debug and write better code.
class Animal {
// Private fields — not accessible outside the class
#name;
#sound;
// Static field
static count = 0;
constructor(name, sound) {
this.#name = name;
this.#sound = sound;
Animal.count++;
}
// Getter — access like a property
get name() { return this.#name; }
// Method
speak() {
return `${this.#name} says ${this.#sound}!`;
}
// Static method — called on class, not instance
static getCount() {
return `${Animal.count} animals created`;
}
}
// Inheritance with extends
class Dog extends Animal {
#breed;
constructor(name, breed) {
super(name, 'Woof'); // must call super() before 'this'
this.#breed = breed;
}
// Override parent method
speak() {
return `${super.speak()} (${this.#breed})`;
}
fetch(item) {
return `${this.name} fetches the ${item}!`;
}
}
const dog = new Dog('Rex', 'Labrador');
console.log(dog.speak()); // 'Rex says Woof! (Labrador)'
console.log(dog.fetch('ball')); // 'Rex fetches the ball!'
console.log(Animal.getCount()); // '1 animals created'
// dog.#name; // SyntaxError — private field not accessible
Map, Set, WeakMap, WeakSet
ES6 introduced proper collection types. Map is a key-value store where any value (including objects) can be a key — unlike plain objects which coerce all keys to strings. Set stores unique values. Their Weak variants hold weak references, enabling garbage collection of keys.
// === MAP ===
const userRoles = new Map();
// Any type as key — even objects
const alice = { id: 1, name: 'Alice' };
userRoles.set(alice, 'admin');
userRoles.set('guestToken', 'viewer');
console.log(userRoles.get(alice)); // 'admin'
console.log(userRoles.has('guestToken')); // true
console.log(userRoles.size); // 2
// Iteration
for (const [key, value] of userRoles) {
console.log(key, '->', value);
}
// Create from entries
const config = new Map([
['host', 'localhost'],
['port', 3000],
['debug', true],
]);
// Convert to/from object
const obj = Object.fromEntries(config);
const map = new Map(Object.entries(obj));
// === SET ===
const tags = new Set(['js', 'react', 'js', 'node', 'react']);
console.log([...tags]); // ['js', 'react', 'node'] — duplicates removed
tags.add('typescript');
tags.delete('node');
console.log(tags.has('react')); // true
// Remove duplicates from array
const arr = [1, 2, 2, 3, 3, 3, 4];
const unique = [...new Set(arr)]; // [1, 2, 3, 4]
// Set operations
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);
const union = new Set([...a, ...b]); // {1,2,3,4,5,6}
const intersection = new Set([...a].filter(x => b.has(x))); // {3,4}
const difference = new Set([...a].filter(x => !b.has(x))); // {1,2}
Optional Chaining and Nullish Coalescing
These two operators (ES2020) dramatically reduce defensive null-checking code. Optional chaining (?.) short-circuits to undefined when a value is null or undefined. Nullish coalescing (??) provides a default only for null/undefined — unlike || which also catches falsy values like 0 and empty strings.
const user = {
profile: {
address: {
city: 'Mumbai'
}
},
getName: () => 'Alice',
scores: [95, 87, 92]
};
// === OPTIONAL CHAINING (?.) ===
// Without optional chaining — verbose and error-prone
const city = user && user.profile && user.profile.address
? user.profile.address.city
: undefined;
// With optional chaining — clean!
const city = user?.profile?.address?.city; // 'Mumbai'
const zip = user?.profile?.address?.zip; // undefined (no error)
// On methods
const name = user?.getName?.(); // 'Alice'
const age = user?.getAge?.(); // undefined (method doesn't exist)
// On arrays
const firstScore = user?.scores?.[0]; // 95
const score4 = user?.scores?.[3]; // undefined
// === NULLISH COALESCING (??) ===
// ?? — only triggers on null or undefined
const port = process.env.PORT ?? 3000;
// If PORT is '0' (falsy), ?? keeps '0' — || would give 3000 (wrong!)
const config = {
timeout: 0, // valid value
retries: null, // explicitly unset
debug: false, // valid value
};
console.log(config.timeout ?? 5000); // 0 (not 5000)
console.log(config.retries ?? 3); // 3 (was null)
console.log(config.debug ?? true); // false (not true)
// Combining both
const displayName = user?.profile?.displayName ?? user?.getName?.() ?? 'Anonymous';
// Nullish assignment (??=) — assign only if null/undefined
config.retries ??= 3; // sets retries to 3 since it was null
config.timeout ??= 5000; // doesn't change — 0 is not null/undefined
ES6+ Quick Reference: Features by Year
| Feature | ES Version | Year |
|---|---|---|
| let, const, Arrow Functions, Classes, Promises, Destructuring, Spread/Rest, Template Literals, Modules, Map/Set, Symbol | ES6 / ES2015 | 2015 |
| Object.values/entries, String padding, Async iterators | ES2017 | 2017 |
| async/await | ES2017 | 2017 |
| Object spread, Promise.finally, for-await-of | ES2018 | 2018 |
| Array flat/flatMap, Object.fromEntries, Optional catch binding | ES2019 | 2019 |
| Optional chaining (?.), Nullish coalescing (??), Promise.allSettled, BigInt, globalThis | ES2020 | 2020 |
| Logical assignment (??=, &&=, ||=), Numeric separators (1_000_000), Promise.any, String replaceAll | ES2021 | 2021 |
| Private class fields (#), Top-level await, Object.hasOwn, Array.at(-1), Error cause | ES2022 | 2022 |
| Array groupBy (Object.groupBy), Promise.withResolvers, Hashbang grammar | ES2024 | 2024 |
Putting It All Together: Real-World Patterns
The true power of ES6+ emerges when you combine features. Here's a realistic data-fetching utility that uses almost everything covered in this cheatsheet:
// api.js — A modern API client using ES6+ features
const BASE_URL = 'https://api.example.com';
class ApiError extends Error {
#statusCode;
constructor(message, statusCode) {
super(message);
this.name = 'ApiError';
this.#statusCode = statusCode;
}
get statusCode() { return this.#statusCode; }
}
async function request(endpoint, {
method = 'GET',
body,
headers = {},
timeout = 5000,
} = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(`${BASE_URL}${endpoint}`, {
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
if (!response.ok) {
throw new ApiError(
`Request failed: ${response.statusText}`,
response.status
);
}
return await response.json();
} finally {
clearTimeout(timeoutId);
}
}
// Named exports — tree-shakeable
export const get = (url, opts) => request(url, { ...opts, method: 'GET' });
export const post = (url, body, opts) => request(url, { ...opts, method: 'POST', body });
export const put = (url, body, opts) => request(url, { ...opts, method: 'PUT', body });
export const del = (url, opts) => request(url, { ...opts, method: 'DELETE' });
// Usage with optional chaining + nullish coalescing
import { get, post } from './api.js';
async function loadUserProfile(userId) {
const [user, posts] = await Promise.all([
get(`/users/${userId}`),
get(`/users/${userId}/posts`),
]);
return {
name: user?.profile?.displayName ?? user?.name ?? 'Anonymous',
bio: user?.profile?.bio ?? '',
postCount: posts?.length ?? 0,
latestPost: posts?.at(-1)?.title ?? 'No posts yet',
};
}
Key Takeaways
- Always use
constby default — only useletwhen you need to reassign. Never usevar. - Arrow functions fix
this— use them for callbacks; use regular functions for methods and constructors. - Destructuring is about intent — it signals exactly which properties you care about from an object or array.
- Spread creates shallow copies — use it for immutable state patterns, merging configs, and variadic function calls.
async/awaitis syntactic sugar over Promises — you still need to understand Promises to use it effectively.- ES modules enable tree-shaking — prefer named exports over default exports for utility libraries.
??over||for defaults — nullish coalescing is almost always what you actually want for default values.- Map and Set have O(1) lookups — use them instead of arrays when you need fast membership testing or uniqueness.
"Any application that can be written in JavaScript, will eventually be written in JavaScript — and with ES6+, it'll be written beautifully."
— Adapted from Jeff Atwood
Mastering ES6+ features isn't just about writing less code — it's about writing clearer code that communicates intent, reduces bugs, and works in harmony with modern tooling. Start applying these features one at a time in your current project: begin with const/let and destructuring, then layer in async/await and modules. Within weeks, writing pre-ES6 JavaScript will feel like going back to dial-up.
