Skip to content

JavaScript ES6+ Reference

JavaScript ES6+ Reference

The modern JavaScript patterns you actually use: destructuring, async/await, modules, classes, optional chaining, and the array/object methods that replace most loops.

Variables — const, let, and why var is done
// const — block-scoped, must be initialised, cannot be reassigned
const API_URL = "https://api.example.com";
const config = { timeout: 5000 };
config.timeout = 10000;    // FINE — mutating the object, not reassigning the binding
// config = {};            // TypeError — cannot reassign const

// let — block-scoped, can be reassigned
let count = 0;
count += 1;

// Temporal dead zone — let/const are hoisted but NOT initialised
// console.log(x);  // ReferenceError
let x = 1;

// var problems (avoid)
// 1. Function-scoped, not block-scoped — leaks out of if/for blocks
// 2. Hoisted AND initialised to undefined — silent bugs
// 3. Can be re-declared in same scope
for (var i = 0; i < 3; i++) {}
console.log(i);   // 3 — leaked!
for (let j = 0; j < 3; j++) {}
// console.log(j);  // ReferenceError — correct

// Rule: default to const. Use let when you need to reassign. Never use var.
Destructuring — arrays and objects
// Object destructuring
const { name, age, city = "London" } = user;   // default value
const { name: fullName } = user;                // rename to fullName
const { address: { street } } = user;           // nested
const { name, ...rest } = user;                 // rest collects remaining keys

// Array destructuring
const [first, second, , fourth] = arr;          // skip index 2
const [head, ...tail] = arr;                    // rest in arrays
const [a, b] = [b, a];                          // swap values (classic)

// Function parameter destructuring
function greet({ name, role = "user" }) {
  return `Hello ${name} (${role})`;
}
greet({ name: "Alice", role: "admin" });

// Destructure in loops
const users = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
for (const { id, name } of users) {
  console.log(id, name);
}

// Destructure fetch response
const { data, status } = await fetchUser(id);

// Rename + default + nested — common in React/API code
const {
  user: {
    profile: { avatar = "/default.png", displayName: name = "Anonymous" } = {}
  } = {}
} = apiResponse;
Spread and rest operators
// Spread — expand iterable into individual elements
const merged = { ...defaults, ...overrides };       // object merge (right wins)
const copy   = { ...original };                     // shallow copy
const arr2   = [...arr1, 4, 5];                     // array concat
const args   = [...arguments];                      // arguments → real array

// Spread for immutable updates (React/Redux pattern)
const updated = { ...state, count: state.count + 1 };
const newList = [...list, newItem];
const removed = list.filter(item => item.id !== id);  // no spread needed

// Spread with function calls
Math.max(...numbers);
console.log(...items);
new Date(...dateParts);

// Rest parameters — collect remaining args into array
function sum(...nums) {
  return nums.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3, 4);    // 10

function first(a, b, ...rest) {
  console.log(a, b, rest);   // rest is a real array
}

// Rest in destructuring
const { id, ...profile } = user;            // profile has everything except id
const [head, ...tail] = [1, 2, 3, 4];      // tail = [2, 3, 4]
Template literals and tagged templates
// Template literals — backtick strings
const greeting = `Hello, ${name}!`;
const multiline = `
  SELECT *
  FROM users
  WHERE id = ${userId}
`;

// Expressions in templates
const msg = `${items.length} item${items.length !== 1 ? "s" : ""}`;
const price = `Total: ${(qty * unitPrice).toFixed(2)}`;
const path = `${base}/${endpoint.replace(/^\//, "")}`;

// Nested template literals
const html = `
    ${items.map(i => `
  • ${i.name}
  • `).join("")}
`; // Tagged templates — tag function receives split strings + values function sql(strings, ...values) { return { text: strings.reduce((q, s, i) => q + s + (i < values.length ? `$${i + 1}` : ""), ""), values }; } const query = sql`SELECT * FROM users WHERE id = ${userId} AND active = ${true}`; // { text: "SELECT * FROM users WHERE id = $1 AND active = $2", values: [1, true] } // String.raw — no escape processing String.raw`C:\Users\${name}\Documents` // backslashes preserved
Arrow functions and this binding
// Arrow function syntax
const double = x => x * 2;
const add    = (a, b) => a + b;
const greet  = name => ({ message: `Hello ${name}` });   // wrap object in ()
const noop   = () => {};

// Multi-line body requires explicit return
const process = (items) => {
  const filtered = items.filter(i => i.active);
  return filtered.map(i => i.name);
};

// Key difference: arrow functions do NOT have their own `this`
// They capture `this` from the surrounding lexical scope
class Timer {
  constructor() {
    this.seconds = 0;
  }
  start() {
    // Arrow: `this` is the Timer instance
    setInterval(() => this.seconds++, 1000);

    // Regular function: `this` would be undefined (strict) or global
    // setInterval(function() { this.seconds++; }, 1000);  // BROKEN
  }
}

// Common patterns
const bound = array
  .filter(item => item.active)
  .map(item => item.name)
  .sort((a, b) => a.localeCompare(b));

// Arrow functions have no `arguments` — use rest params
const logAll = (...args) => console.log(...args);

// Arrow functions cannot be constructors
// const Obj = () => {};  new Obj();  // TypeError
Async/await and Promises
// Promise basics
fetch("/api/user")
  .then(res => res.json())
  .then(user => console.log(user))
  .catch(err => console.error(err))
  .finally(() => setLoading(false));

// async/await — cleaner sequential async
async function loadUser(id) {
  try {
    const res  = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const user = await res.json();
    return user;
  } catch (err) {
    console.error("Failed to load user:", err);
    throw err;  // re-throw so callers can handle it
  }
}

// Parallel execution — don't await one by one if independent
// SLOW — sequential:
const user    = await fetchUser(id);
const posts   = await fetchPosts(id);

// FAST — parallel:
const [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]);

// Promise.allSettled — get all results even if some fail
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
for (const result of results) {
  if (result.status === "fulfilled") console.log(result.value);
  else console.error(result.reason);
}

// Promise.race — first to resolve/reject wins
const data = await Promise.race([fetch("/api/data"), timeout(5000)]);

// Promise.any — first to resolve (ignores rejections until all fail)
const fastest = await Promise.any([fetchPrimary(), fetchFallback()]);

// async IIFE — top-level await alternative (pre-ESM)
(async () => {
  const data = await loadData();
  console.log(data);
})();

// Top-level await (ESM modules only)
const config = await fetchConfig();    // valid at module top level

Common mistake: await inside .map() doesn't work — you get an array of Promises. Use await Promise.all(arr.map(async item => ...)).

Array methods — the ones that replace most loops
const users = [
  { id: 1, name: "Alice", role: "admin", active: true,  score: 95 },
  { id: 2, name: "Bob",   role: "user",  active: false, score: 72 },
  { id: 3, name: "Carol", role: "user",  active: true,  score: 88 },
];

// map — transform each element, returns new array
const names  = users.map(u => u.name);               // ["Alice", "Bob", "Carol"]
const cards  = users.map(u => ({ ...u, label: `#${u.id}` }));

// filter — keep elements matching predicate
const active = users.filter(u => u.active);
const admins = users.filter(u => u.role === "admin");

// find / findIndex — first match
const alice  = users.find(u => u.name === "Alice");
const idx    = users.findIndex(u => u.id === 3);      // 2

// reduce — aggregate to single value
const total  = users.reduce((sum, u) => sum + u.score, 0);   // 255
const byId   = users.reduce((map, u) => ({ ...map, [u.id]: u }), {});
// { 1: Alice, 2: Bob, 3: Carol }

// some / every — boolean checks
const anyAdmin    = users.some(u => u.role === "admin");    // true
const allActive   = users.every(u => u.active);             // false

// flat / flatMap
const nested    = [[1, 2], [3, 4]];
const flat      = nested.flat();                   // [1, 2, 3, 4]
const sentences = ["hello world", "foo bar"];
const words     = sentences.flatMap(s => s.split(" "));  // ["hello","world","foo","bar"]

// sort — mutates! copy first
const sorted = [...users].sort((a, b) => b.score - a.score);   // descending
const alpha  = [...users].sort((a, b) => a.name.localeCompare(b.name));

// includes / indexOf
[1, 2, 3].includes(2);         // true
["a", "b"].indexOf("b");       // 1

// Array.from — create array from iterable or array-like
Array.from({ length: 5 }, (_, i) => i);    // [0, 1, 2, 3, 4]
Array.from(new Set([1, 1, 2, 2, 3]));      // [1, 2, 3] — dedup
Array.from(document.querySelectorAll("p")); // NodeList → Array

// at() — negative indexing (ES2022)
arr.at(-1);     // last element
arr.at(-2);     // second to last
Objects — modern patterns
// Shorthand properties and methods
const name = "Alice", age = 30;
const user = { name, age };    // { name: "Alice", age: 30 }

const obj = {
  value: 42,
  getValue() { return this.value; },           // method shorthand
  get doubled() { return this.value * 2; },    // getter
  set doubled(v) { this.value = v / 2; },      // setter
  [computedKey]: "dynamic",                    // computed property name
};

// Object.keys / values / entries
Object.keys(obj)                      // ["a", "b", ...]
Object.values(obj)                    // [1, 2, ...]
Object.entries(obj)                   // [["a", 1], ["b", 2], ...]

// Rebuild object from entries (transform values)
Object.fromEntries(
  Object.entries(config).filter(([, v]) => v !== null)
);

// Object.assign — shallow merge (prefer spread)
const merged = Object.assign({}, defaults, overrides);
const copy   = Object.assign({}, obj);

// Object.freeze — immutable (shallow)
const CONSTANTS = Object.freeze({ MAX: 100, MIN: 0 });

// Nullish coalescing (??) — only for null/undefined
const port = config.port ?? 3000;    // 0 is valid — ?? won't override it
// vs:
const port2 = config.port || 3000;  // 0 would be replaced — usually wrong

// Optional chaining (?.)
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

// Logical assignment
x ??= "default";       // assign only if x is null/undefined
x ||= "fallback";      // assign only if x is falsy
x &&= transform(x);    // assign only if x is truthy
Classes
class Animal {
  // Private fields (ES2022) — enforced at engine level, not just convention
  #name;
  #sound;

  static count = 0;                    // static field

  constructor(name, sound) {
    this.#name  = name;
    this.#sound = sound;
    Animal.count++;
  }

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

  get name() { return this.#name; }    // getter

  static create(name, sound) {         // factory method
    return new Animal(name, sound);
  }
}

class Dog extends Animal {
  #tricks = [];

  constructor(name) {
    super(name, "woof");               // must call super() before using this
  }

  learn(trick) {
    this.#tricks.push(trick);
    return this;                       // fluent interface / method chaining
  }

  speak() {
    const base = super.speak();        // call parent method
    return `${base}! (trained: ${this.#tricks.join(", ")})`;
  }
}

const dog = new Dog("Rex").learn("sit").learn("stay");
dog.speak();    // "Rex says woof! (trained: sit, stay)"

// Mixins — JS has no multiple inheritance, use mixins
const Serializable = (Base) => class extends Base {
  toJSON() { return JSON.stringify(this); }
  static fromJSON(json) { return Object.assign(new this(), JSON.parse(json)); }
};

class User extends Serializable(Animal) {}
Modules — import / export
// Named exports — math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export class Vector { constructor(x, y) { this.x = x; this.y = y; } }

// Default export — one per module
export default function main() { /* ... */ }

// Re-export (barrel file — index.js)
export { add, PI }    from "./math.js";
export { default as Vector } from "./vector.js";
export * from "./utils.js";

// Imports
import main               from "./main.js";          // default
import { add, PI }        from "./math.js";           // named
import { add as sum }     from "./math.js";           // rename
import * as math          from "./math.js";           // namespace import
import main, { add }      from "./module.js";         // both

// Dynamic import — lazy load (returns Promise)
const { add } = await import("./math.js");
const module  = await import(`./plugins/${name}.js`);

// import.meta
console.log(import.meta.url);      // URL of current module
console.log(import.meta.env);      // Vite/bundler env vars

// JSON modules (Node 22+, --experimental-json-modules in older)
import data from "./data.json" assert { type: "json" };

// CommonJS (Node.js legacy — still common in older packages)
const fs = require("fs");
module.exports = { add };
// Interop: import cjs from "./legacy.cjs";   // default import of module.exports
Map, Set, WeakMap, WeakSet
// Map — key-value, any type as key, preserves insertion order
const map = new Map();
map.set("key", "value");
map.set(objKey, data);        // objects as keys — unlike plain objects
map.get("key");               // "value"
map.has("key");               // true
map.delete("key");
map.size;                     // number of entries

// Iterate
for (const [key, value] of map) { /* ... */ }
[...map.keys()]
[...map.values()]
[...map.entries()]

// Build from array of pairs
const map2 = new Map([["a", 1], ["b", 2]]);

// Set — unique values, any type, preserves insertion order
const set = new Set([1, 2, 3, 2, 1]);    // {1, 2, 3}
set.add(4);
set.has(2);       // true
set.delete(1);
set.size;         // 3

// Deduplication (the main use case)
const unique = [...new Set(arr)];
const uniqueById = [...new Map(users.map(u => [u.id, u])).values()];

// Set operations (ES2025 methods — polyfill may be needed)
setA.union(setB)
setA.intersection(setB)
setA.difference(setB)

// WeakMap — keys must be objects, NOT iterable, no .size
// Allows GC to collect keys when no other references exist
// Use case: attach private data to DOM nodes or class instances without memory leaks
const metadata = new WeakMap();
metadata.set(element, { clicks: 0 });

// WeakSet — objects only, NOT iterable
// Use case: track "visited" objects without preventing GC
const visited = new WeakSet();
visited.add(node);
Symbols, iterators, and generators
// Symbol — unique, immutable primitive, used for non-string keys
const id = Symbol("id");
const obj = { [id]: 123, name: "Alice" };
obj[id];             // 123
Object.keys(obj);    // ["name"] — symbols are not enumerable

// Well-known symbols — hook into JS language behaviour
class Range {
  constructor(start, end) { this.start = start; this.end = end; }

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

for (const n of new Range(1, 5)) console.log(n);  // 1 2 3 4 5
[...new Range(1, 3)];                              // [1, 2, 3]

// Generators — functions that pause execution
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
fib.next().value;   // 0
fib.next().value;   // 1
fib.next().value;   // 1

// Async generators — for streaming/pagination
async function* paginate(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    const { data, nextUrl } = await res.json();
    yield data;
    next = nextUrl;
  }
}

for await (const page of paginate("/api/items")) {
  process(page);
}
Error handling patterns
// Custom error classes
class AppError extends Error {
  constructor(message, code, statusCode = 500) {
    super(message);
    this.name = this.constructor.name;
    this.code = code;
    this.statusCode = statusCode;
  }
}

class NotFoundError extends AppError {
  constructor(resource, id) {
    super(`${resource} ${id} not found`, "NOT_FOUND", 404);
  }
}

// throw and instanceof
throw new NotFoundError("User", 42);

try {
  await loadUser(id);
} catch (err) {
  if (err instanceof NotFoundError) redirect("/404");
  else if (err instanceof AppError) showError(err.message);
  else throw err;   // re-throw unexpected errors
}

// Error cause (ES2022) — chain errors with context
try {
  await db.query(sql);
} catch (cause) {
  throw new AppError("Database query failed", "DB_ERROR", 500, { cause });
}

// Catching without binding (ES2019)
try { JSON.parse(str); }
catch { return null; }   // no `err` variable needed

// Promise error handling
loadUser(id)
  .then(process)
  .catch(err => {
    if (err instanceof NotFoundError) return null;
    throw err;
  });

// Global unhandled rejection handler (Node.js)
process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled rejection:", reason);
  process.exit(1);
});

🔍 Free tool: npm Package Health Checker — check any npm package — React, Lodash, Axios — for EOL status, known CVEs, and active maintenance in seconds.

📦 Free tool: package.json Health Checker — paste your entire package.json and get A–F health grades for all dependencies at once, including React, Vue, TypeScript, and build tools.

Founded

2023 in London, UK

Contact

hello@releaserun.com