
Modern Javascript
- 209 installs
- 57 repo stars
- Updated July 7, 2026
- ccheney/robust-skills
Write maintainable modern JavaScript/TypeScript using ES modules, async patterns, and current language features in app and tooling code.
About
Modern-javascript from ccheney/robust-skills equips agents to author robust client and tooling code with current ECMAScript: modules, async control flow, functional patterns, lint-friendly style, and bundler-compatible structure for SaaS, extensions, and CLI frontends.
- ES modules and import maps
- Async/await and promises
- Modern syntax and tooling
- Tree-shakeable client patterns
Modern Javascript by the numbers
- 209 all-time installs (skills.sh)
- Ranked #846 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ccheney/robust-skills --skill modern-javascriptAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 209 |
|---|---|
| repo stars | ★ 57 |
| Last updated | July 7, 2026 |
| Repository | ccheney/robust-skills ↗ |
What it does
Write maintainable modern JavaScript/TypeScript using ES modules, async patterns, and current language features in app and tooling code.
Files
Modern JavaScript (ES6-ES2025)
Write clean, performant, maintainable JavaScript using modern language features. This skill covers ES6 through ES2025, emphasizing immutability, functional patterns, and expressive syntax.
Quick Decision Trees
"Which array method should I use?"
What do I need?
├─ Transform each element → .map()
├─ Keep some elements → .filter()
├─ Find one element → .find() / .findLast()
├─ Check if condition met → .some() / .every()
├─ Reduce to single value → .reduce()
├─ Get last element → .at(-1)
├─ Sort without mutating → .toSorted()
├─ Reverse without mutating → .toReversed()
├─ Group by property → Object.groupBy()
└─ Flatten nested arrays → .flat() / .flatMap()"How do I handle nullish values?"
Nullish handling?
├─ Safe property access → obj?.prop / obj?.[key]
├─ Safe method call → obj?.method?.()
├─ Default for null/undefined only → value ?? 'default'
├─ Default for any falsy → value || 'default'
├─ Assign if null/undefined → obj.prop ??= 'default'
└─ Check property exists → Object.hasOwn(obj, 'key')"Should I mutate or copy?"
Always prefer non-mutating methods:
├─ Sort array → .toSorted() (not .sort())
├─ Reverse array → .toReversed() (not .reverse())
├─ Splice array → .toSpliced() (not .splice())
├─ Update element → .with(i, val) (not arr[i] = val)
├─ Add to array → [...arr, item] (not .push())
└─ Merge objects → {...obj, key} (not Object.assign())ES Version Quick Reference
| Version | Year | Key Features |
|---|---|---|
| ES6 | 2015 | let/const, arrow functions, classes, destructuring, spread, Promises, modules, Symbol, Map/Set, Proxy, generators |
| ES2016 | 2016 | Array.includes(), exponentiation operator ** |
| ES2017 | 2017 | async/await, Object.values/entries, padStart/padEnd, trailing commas, SharedArrayBuffer, Atomics |
| ES2018 | 2018 | Rest/spread for objects, for await...of, Promise.finally(), RegExp named groups, lookbehind, dotAll flag |
| ES2019 | 2019 | .flat(), .flatMap(), Object.fromEntries(), trimStart/End(), optional catch binding, stable Array.sort() |
| ES2020 | 2020 | Optional chaining ?., nullish coalescing ??, BigInt, Promise.allSettled(), globalThis, dynamic import() |
| ES2021 | 2021 | String.replaceAll(), Promise.any(), logical assignment ??= and or=, numeric separators 1_000_000 |
| ES2022 | 2022 | .at(), Object.hasOwn(), top-level await, private class fields #field, static blocks, Error.cause |
| ES2023 | 2023 | .toSorted(), .toReversed(), .toSpliced(), .with(), .findLast(), .findLastIndex(), hashbang grammar |
| ES2024 | 2024 | Object.groupBy(), Map.groupBy(), Promise.withResolvers(), RegExp v flag, resizable ArrayBuffer |
| ES2025 | 2025 | Iterator helpers (.map, .filter, .take), Set methods (.union, .intersection), RegExp.escape(), using/await using |
Modernization Patterns
Array Access
// ❌ Legacy
const last = arr[arr.length - 1];
const secondLast = arr[arr.length - 2];
// ✅ Modern (ES2022)
const last = arr.at(-1);
const secondLast = arr.at(-2);Non-Mutating Array Operations
// ❌ Mutates original array
const sorted = arr.sort((a, b) => a - b);
const reversed = arr.reverse();
// ✅ Returns new array (ES2023)
const sorted = arr.toSorted((a, b) => a - b);
const reversed = arr.toReversed();
const updated = arr.with(2, 'new value');
const removed = arr.toSpliced(1, 1);String Replacement
// ❌ Legacy with regex
const result = str.replace(/foo/g, 'bar');
// ✅ Modern (ES2021)
const result = str.replaceAll('foo', 'bar');Grouping Data
// ❌ Manual grouping
const grouped = items.reduce((acc, item) => {
const key = item.category;
acc[key] = acc[key] || [];
acc[key].push(item);
return acc;
}, {});
// ✅ Modern (ES2024)
const grouped = Object.groupBy(items, item => item.category);Nullish Handling
// ❌ Falsy check (0, '', false are valid values)
const value = input || 'default';
const name = user && user.profile && user.profile.name;
// ✅ Nullish check (only null/undefined)
const value = input ?? 'default';
const name = user?.profile?.name;Property Existence
// ❌ Can be fooled by prototype or overwritten hasOwnProperty
if (obj.hasOwnProperty('key')) { }
// ✅ Modern (ES2022)
if (Object.hasOwn(obj, 'key')) { }Logical Assignment
// ❌ Verbose assignment
if (obj.prop === null || obj.prop === undefined) {
obj.prop = 'default';
}
// ✅ Modern (ES2021)
obj.prop ??= 'default'; // Assign if null/undefined
obj.count ||= 0; // Assign if falsy
obj.enabled &&= check(); // Assign if truthyAsync Patterns
Promise Combinators
// Wait for all, fail if any fails
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);
// Wait for all, get status of each
const results = await Promise.allSettled([fetchA(), fetchB()]);
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.error(r.reason);
});
// First to succeed
const fastest = await Promise.any([fetchFromCDN1(), fetchFromCDN2()]);
// First to settle
const winner = await Promise.race([fetchData(), timeout(5000)]);Promise.withResolvers (ES2024)
// ❌ Legacy pattern
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// ✅ Modern (ES2024)
const { promise, resolve, reject } = Promise.withResolvers();Top-Level Await (ES2022)
// In ES modules, await at top level
const config = await fetch('/config.json').then(r => r.json());
const db = await connectDatabase(config);
export { db };Functional Patterns
Immutable Object Updates
// Add/update property
const updated = { ...user, age: 31 };
// Remove property
const { password, ...userWithoutPassword } = user;
// Nested update
const updated = {
...state,
user: { ...state.user, name: 'New Name' }
};Array Transformations
// Chain transformations (ES2023)
const result = users
.filter(u => u.active)
.map(u => u.name)
.toSorted();
// Using flatMap for filter+map (single pass)
const activeNames = users.flatMap(u => u.active ? [u.name] : []);
// ES2024: Group then process
const byStatus = Object.groupBy(users, u => u.active ? 'active' : 'inactive');
const activeNames = byStatus.active?.map(u => u.name) ?? [];Composition
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const processUser = pipe(
user => ({ ...user, name: user.name.trim() }),
user => ({ ...user, email: user.email.toLowerCase() }),
user => ({ ...user, createdAt: new Date() })
);Destructuring Patterns
Object Destructuring
// Basic with rename and default
const { name: userName, age = 18 } = user;
// Nested
const { address: { city, country } } = user;
// Rest
const { id, ...userData } = user;Array Destructuring
// Skip elements
const [first, , third] = array;
// Rest
const [head, ...tail] = array;
// Swap variables
[a, b] = [b, a];
// Function returns
const [x, y] = getCoordinates();Anti-Patterns
| Anti-Pattern | Problem | Modern Solution |
|---|---|---|
arr[arr.length-1] | Verbose, error-prone | arr.at(-1) |
.sort() on original | Mutates array | .toSorted() |
.replace(/g/) for all | Regex overhead | .replaceAll() |
obj.hasOwnProperty() | Can be overwritten | Object.hasOwn() |
| `value \ | \ | default` |
obj && obj.prop && obj.prop.method() | Verbose null checks | obj?.prop?.method?.() |
for (let i = 0; ...) | Index bugs, verbose | .map(), .filter(), for...of |
new Promise((res, rej) => ...) | Boilerplate | Promise.withResolvers() |
| Manual array grouping | Verbose, error-prone | Object.groupBy() |
Best Practices
1. Use `const` by default — Only use let when reassignment is needed 2. Prefer arrow functions — Especially for callbacks and short functions 3. Use template literals — Instead of string concatenation 4. Destructure early — Extract what you need at function start 5. Avoid mutations — Use .toSorted(), .toReversed(), spread operator 6. Use optional chaining — Prevent "Cannot read property of undefined" 7. Use nullish coalescing — ?? for defaults, not || (unless intentional) 8. Prefer array methods — .map(), .filter(), .find() over loops 9. Use `async/await` — Instead of .then() chains 10. Handle errors properly — try/catch with async/await
Reference Documentation
ES Version References
| File | Purpose |
|---|---|
| references/ES2016-ES2017.md | includes, async/await, Object.values/entries, string padding |
| references/ES2018-ES2019.md | rest/spread objects, flat/flatMap, RegExp named groups |
| references/ES2022-ES2023.md | .at(), .toSorted(), .toReversed(), .findLast(), class features |
| references/ES2024.md | Object.groupBy, Promise.withResolvers, RegExp v flag |
| references/ES2025.md | Set methods, iterator helpers, using/await using |
| references/UPCOMING.md | Temporal API, Decorators, Decorator Metadata |
Pattern References
| File | Purpose |
|---|---|
| references/PROMISES.md | Promise fundamentals, async/await, combinators |
| references/CONCURRENCY.md | Parallel, batched, pool patterns, retry, cancellation |
| references/IMMUTABILITY.md | Immutable data patterns, pure functions |
| references/COMPOSITION.md | Higher-order functions, memoization, monads |
| references/CHEATSHEET.md | Quick syntax reference |
Resources
Specifications
- ECMAScript Specification: https://tc39.es/ecma262/ (living standard)
- TC39 Proposals: https://github.com/tc39/proposals (upcoming features)
- TC39 Process: https://tc39.es/process-document/ (how features are added)
Documentation
- MDN Web Docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript
- JavaScript.info: https://javascript.info/
Compatibility
- Can I Use: https://caniuse.com (browser support tables)
- Node.js ES Compatibility: https://node.green/
Modern JavaScript Cheatsheet
Variables, arrow functions, destructuring, spread/rest, template literals, optional chaining, nullish coalescing, array methods, string methods, object methods, promises, async/await, classes, modules, Set/Map, iterators, generators, RegExp, BigInt, Temporal, resource management.
Variables & Scope
const x = 1; // Block-scoped, cannot reassign
let y = 2; // Block-scoped, can reassign
// var z = 3; // Function-scoped, avoidArrow Functions
const add = (a, b) => a + b; // Expression body
const greet = name => `Hello ${name}`; // Single param, no parens
const getObj = () => ({ key: 'value' }); // Return object literal
const multi = (a, b) => { // Block body
const sum = a + b;
return sum * 2;
};Destructuring
// Object
const { name, age = 18 } = user;
const { name: n, address: { city } } = user;
const { id, ...rest } = user;
// Array
const [first, second] = arr;
const [head, ...tail] = arr;
const [, , third] = arr;
[a, b] = [b, a]; // SwapSpread & Rest
// Spread
const merged = [...arr1, ...arr2];
const clone = { ...obj };
const updated = { ...obj, key: 'new' };
Math.max(...numbers);
// Rest
const sum = (...nums) => nums.reduce((a, b) => a + b);
const { id, ...userData } = user;Template Literals
const str = `Hello ${name}!`;
const multi = `Line 1
Line 2`;
const html = `<div class="${cls}">${content}</div>`;Object Shorthand
const name = 'Alice';
const obj = {
name, // Property shorthand
greet() { }, // Method shorthand
[`key_${id}`]: value, // Computed property
};Optional Chaining & Nullish Coalescing
obj?.prop // Property access
obj?.[key] // Dynamic property
obj?.method?.() // Method call
arr?.[0] // Array access
value ?? 'default' // Nullish coalescing (null/undefined only)
value || 'default' // Falsy coalescing (0, '', false, null, undefined)
obj.prop ??= 'default' // Assign if nullish
obj.prop ||= 'default' // Assign if falsy
obj.prop &&= newValue // Assign if truthyArray Methods
// Transform
arr.map(x => x * 2)
arr.filter(x => x > 0)
arr.reduce((acc, x) => acc + x, 0)
arr.flatMap(x => [x, x * 2])
arr.flat(2)
// Search
arr.find(x => x.id === 1)
arr.findIndex(x => x.id === 1)
arr.findLast(x => x > 5) // ES2023
arr.findLastIndex(x => x > 5) // ES2023
arr.includes(value)
arr.indexOf(value)
// Check
arr.some(x => x > 0)
arr.every(x => x > 0)
// Access
arr.at(-1) // ES2022 - last element
arr.at(-2) // second to last
// Non-mutating (ES2023)
arr.toSorted((a, b) => a - b)
arr.toReversed()
arr.toSpliced(1, 1)
arr.with(0, 'new')
// Group (ES2024)
Object.groupBy(arr, x => x.type)
Map.groupBy(arr, x => x.type)
// Create
Array.from({ length: 5 }, (_, i) => i)
Array.of(1, 2, 3)
await Array.fromAsync(asyncIterable) // ES2025String Methods
str.includes('sub')
str.startsWith('pre')
str.endsWith('suf')
str.padStart(10, '0') // ES2017
str.padEnd(10, '-') // ES2017
str.repeat(3)
str.trim()
str.trimStart() // ES2019
str.trimEnd() // ES2019
str.matchAll(/pattern/g) // ES2020 - iterator of matches
str.replaceAll('a', 'b') // ES2021
str.at(-1) // ES2022 - last char
str.isWellFormed() // ES2024
str.toWellFormed() // ES2024Object Methods
Object.keys(obj)
Object.values(obj)
Object.entries(obj)
Object.fromEntries(entries)
Object.assign({}, obj, updates)
Object.hasOwn(obj, 'key') // ES2022
Object.groupBy(arr, fn) // ES2024Promises
// Create
new Promise((resolve, reject) => { })
Promise.resolve(value)
Promise.reject(error)
Promise.withResolvers() // ES2024
Promise.try(() => fn()) // ES2025
// Combinators
Promise.all([p1, p2, p3])
Promise.allSettled([p1, p2]) // ES2020
Promise.race([p1, p2])
Promise.any([p1, p2]) // ES2021
// Instance methods
promise.then(onFulfilled, onRejected)
promise.catch(onRejected)
promise.finally(onFinally) // ES2018Async/Await
async function fn() {
try {
const result = await promise;
return result;
} catch (error) {
handleError(error);
}
}
// Parallel
const [a, b] = await Promise.all([fetchA(), fetchB()]);
// Sequential
for (const item of items) {
await process(item);
}Classes
class Animal {
#privateField = 0; // Private field
static count = 0; // Static field
constructor(name) {
this.name = name;
Animal.count++;
}
speak() { // Method
return `${this.name} speaks`;
}
#privateMethod() { } // Private method
get displayName() { // Getter
return this.name.toUpperCase();
}
set displayName(value) { // Setter
this.name = value.toLowerCase();
}
static create(name) { // Static method
return new Animal(name);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
return `${super.speak()}: Woof!`;
}
}Modules
// Export
export const x = 1;
export function fn() { }
export default class { }
export { a, b as c };
// Import
import Default from './module';
import { x, fn } from './module';
import { x as y } from './module';
import * as mod from './module';
import './module'; // Side effects only
// Dynamic
const mod = await import('./module');Set & Map
// Set
const set = new Set([1, 2, 3]);
set.add(4);
set.has(1);
set.delete(1);
set.size;
set.clear();
// ES2025 Set methods
setA.union(setB)
setA.intersection(setB)
setA.difference(setB)
setA.symmetricDifference(setB)
setA.isSubsetOf(setB)
setA.isSupersetOf(setB)
setA.isDisjointFrom(setB)
// Map
const map = new Map([['a', 1], ['b', 2]]);
map.set('c', 3);
map.get('a');
map.has('a');
map.delete('a');
map.size;Iterators & Generators
// Generator
function* gen() {
yield 1;
yield 2;
yield 3;
}
// Async generator
async function* asyncGen() {
yield await fetch(url1);
yield await fetch(url2);
}
// for...of
for (const x of iterable) { }
for await (const x of asyncIterable) { } // ES2018
// Iterator helpers (ES2025)
iter.map(fn)
iter.filter(fn)
iter.take(n)
iter.drop(n)
iter.toArray()
Iterator.from(iterable)Regular Expressions
// Named capture groups (ES2018)
const pattern = /(?<year>\d{4})-(?<month>\d{2})/;
const { year, month } = str.match(pattern).groups;
// Lookbehind assertions (ES2018)
/(?<=\$)\d+/ // Positive lookbehind
/(?<!\$)\d+/ // Negative lookbehind
// Flags
/pattern/g // Global
/pattern/i // Case-insensitive
/pattern/m // Multiline
/pattern/s // dotAll - . matches newlines (ES2018)
/pattern/u // Unicode
/pattern/d // Match indices (ES2022)
/pattern/v // Unicode sets (ES2024)
// Unicode property escapes (ES2018)
/\p{Letter}/u // Any letter
/\p{Emoji}/u // Emoji
/\p{Script=Greek}/u // Greek script
// Unicode set operations (ES2024 /v flag)
/[\p{Emoji}--\p{ASCII}]/v // Emoji minus ASCII
/[[a-z]&&[^aeiou]]/v // Consonants only
// Match indices (ES2022)
const match = /(?<g>\w+)/.exec('hello');
match.indices.groups.g // [0, 5]
// RegExp.escape (ES2025)
RegExp.escape('$100') // '\\$100'Primitives, Errors, Cloning, Resource Management
// BigInt (ES2020)
const big = 9007199254740991n;
BigInt(123);
// Numeric separators (ES2021)
const million = 1_000_000;
// globalThis (ES2020)
globalThis.setTimeout
// Error cause (ES2022)
throw new Error('msg', { cause: originalError });
// structuredClone
const deep = structuredClone(obj);
// String well-formed (ES2024)
str.isWellFormed() // Check for lone surrogates
str.toWellFormed() // Fix lone surrogates
// Private slot check (ES2022)
#field in obj // true if obj has #field
// Static initialization blocks (ES2022)
class C {
static {
// Runs when class is defined
}
}
// Promise.finally (ES2018)
promise.finally(() => cleanup())
// Promise.try (ES2025)
Promise.try(() => mayThrow())
// Float16 (ES2025)
new Float16Array([1.5, 2.5])
Math.f16round(1.337)
// Import attributes (ES2025)
import data from './data.json' with { type: 'json' }
// WeakRef & FinalizationRegistry (ES2021)
const ref = new WeakRef(obj);
ref.deref() // obj or undefined
// Symbol.description (ES2019)
Symbol('name').description // 'name'
// Optional catch binding (ES2019)
try { } catch { } // No parameter needed
// Hashbang (ES2023)
#!/usr/bin/env node // At file start
// Explicit Resource Management (ES2025)
using file = openFile('data.txt'); // Auto-disposed
await using db = await connect(); // Async disposal
Symbol.dispose // Cleanup method
Symbol.asyncDispose // Async cleanup
new DisposableStack() // Aggregate disposables
// Array.fromAsync (ES2025)
await Array.fromAsync(asyncIterable)
await Array.fromAsync(generator(), mapFn)
// Error.isError (ES2025)
Error.isError(err) // true for any Error, cross-realm safe
// Intl.DurationFormat (ES2025)
new Intl.DurationFormat('en', { style: 'long' })
.format({ hours: 1, minutes: 30 }) // "1 hour, 30 minutes"
// Temporal API (Stage 3 - requires polyfill)
Temporal.PlainDate.from('2024-03-15') // Date only
Temporal.PlainTime.from('14:30:00') // Time only
Temporal.PlainDateTime.from('...') // Date + time
Temporal.ZonedDateTime.from('...[TZ]') // With timezone
Temporal.Now.instant() // Current moment
Temporal.Duration.from({ hours: 2 }) // Duration
date.add({ months: 1 }) // Immutable arithmeticStage 3 Proposals (Decorators, Decorator Metadata)
// Decorators (Stage 3 - requires Babel or TypeScript 5.0+)
@logged
class User {
@validate name;
@memoize getData() { }
}
// Decorator Metadata (Stage 3)
User[Symbol.metadata] // { name: 'string', ... }Quick Migration Guide
| Legacy | Modern |
|---|---|
var x = 1 | const x = 1 or let x = 1 |
function(x) { return x * 2 } | x => x * 2 |
arr[arr.length - 1] | arr.at(-1) |
arr.sort() | arr.toSorted() |
arr.reverse() | arr.toReversed() |
arr.splice(i, 1) | arr.toSpliced(i, 1) |
arr[i] = val | arr.with(i, val) |
str.replace(/a/g, 'b') | str.replaceAll('a', 'b') |
obj.hasOwnProperty('k') | Object.hasOwn(obj, 'k') |
a && a.b && a.b.c | a?.b?.c |
| `x \ | \ |
Object.assign({}, a, b) | { ...a, ...b } |
[].concat(a, b) | [...a, ...b] |
.then().catch() | async/await + try/catch |
let resolve; new Promise(r => resolve = r) | Promise.withResolvers() |
new Date() | Temporal.Now.* (polyfill) |
| Manual grouping with reduce | Object.groupBy() |
Function Composition and Higher-Order Functions
Currying, partial application, pipe, compose, point-free style, memoization with TTL and LRU, Maybe monad, Result monad, transducers, debounce, throttle, once.
Higher-Order Functions
Functions as Arguments
// Map
const doubled = [1, 2, 3].map(x => x * 2);
// Filter
const evens = [1, 2, 3, 4].filter(x => x % 2 === 0);
// Reduce
const sum = [1, 2, 3].reduce((acc, x) => acc + x, 0);
// Custom higher-order function
function unless(predicate, fn) {
return (...args) => {
if (!predicate(...args)) {
return fn(...args);
}
};
}
const logUnlessEmpty = unless(
arr => arr.length === 0,
arr => console.log(arr)
);Functions Returning Functions
// Currying
const add = a => b => a + b;
const add5 = add(5);
add5(3); // 8
// Partial application
const partial = (fn, ...presetArgs) =>
(...laterArgs) => fn(...presetArgs, ...laterArgs);
const greet = (greeting, name) => `${greeting}, ${name}!`;
const sayHello = partial(greet, 'Hello');
sayHello('Alice'); // "Hello, Alice!"
// Closure for state
function counter(start = 0) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
get: () => count
};
}Function Composition
Compose and Pipe
// Right-to-left composition
const compose = (...fns) => x =>
fns.reduceRight((acc, fn) => fn(acc), x);
// Left-to-right composition (pipe)
const pipe = (...fns) => x =>
fns.reduce((acc, fn) => fn(acc), x);
// Usage
const processText = pipe(
str => str.trim(),
str => str.toLowerCase(),
str => str.replace(/\s+/g, '-')
);
processText(' Hello World '); // 'hello-world'Point-Free Style
// With arguments
const getNames = users => users.map(user => user.name);
// Point-free
const prop = key => obj => obj[key];
const map = fn => arr => arr.map(fn);
const getNames = pipe(
map(prop('name'))
);
// More examples
const isPositive = n => n > 0;
const not = fn => (...args) => !fn(...args);
const isNegative = not(isPositive);
const filter = fn => arr => arr.filter(fn);
const getPositive = filter(isPositive);Memoization
Basic Memoization
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const fibonacci = memoize(n => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});Memoization with TTL
function memoizeWithTTL(fn, ttl) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.value;
}
const value = fn(...args);
cache.set(key, { value, timestamp: Date.now() });
return value;
};
}
const fetchUser = memoizeWithTTL(
id => fetch(`/api/users/${id}`).then(r => r.json()),
60000 // 1 minute TTL
);Memoization with Size Limit
function memoizeLRU(fn, maxSize = 100) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
const value = cache.get(key);
cache.delete(key);
cache.set(key, value); // Move to end
return value;
}
const result = fn(...args);
cache.set(key, result);
if (cache.size > maxSize) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
return result;
};
}Functor and Monad Patterns
Maybe (Optional)
class Maybe {
constructor(value) {
this.value = value;
}
static of(value) {
return new Maybe(value);
}
isNothing() {
return this.value === null || this.value === undefined;
}
map(fn) {
return this.isNothing() ? this : Maybe.of(fn(this.value));
}
flatMap(fn) {
return this.isNothing() ? this : fn(this.value);
}
getOrElse(defaultValue) {
return this.isNothing() ? defaultValue : this.value;
}
}
// Usage
const user = Maybe.of({ name: 'Alice', address: { city: 'NYC' } });
const city = user
.map(u => u.address)
.map(a => a.city)
.getOrElse('Unknown');
// Or use optional chaining (simpler for most cases)
const city = user?.address?.city ?? 'Unknown';Result (Either)
class Result {
constructor(value, error) {
this.value = value;
this.error = error;
}
static ok(value) {
return new Result(value, null);
}
static err(error) {
return new Result(null, error);
}
isOk() {
return this.error === null;
}
map(fn) {
return this.isOk() ? Result.ok(fn(this.value)) : this;
}
mapError(fn) {
return this.isOk() ? this : Result.err(fn(this.error));
}
unwrap() {
if (this.isOk()) return this.value;
throw this.error;
}
unwrapOr(defaultValue) {
return this.isOk() ? this.value : defaultValue;
}
}
// Usage
function divide(a, b) {
if (b === 0) return Result.err('Division by zero');
return Result.ok(a / b);
}
const result = divide(10, 2)
.map(x => x * 2)
.map(x => x + 1)
.unwrapOr(0); // 11Transducers (Advanced)
// Composable transformations without intermediate arrays
const map = fn => reducer => (acc, x) => reducer(acc, fn(x));
const filter = pred => reducer => (acc, x) => pred(x) ? reducer(acc, x) : acc;
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
const transduce = (xform, reducer, init, coll) =>
coll.reduce(xform(reducer), init);
// Usage
const xform = compose(
filter(x => x % 2 === 0),
map(x => x * 2)
);
const result = transduce(
xform,
(acc, x) => [...acc, x],
[],
[1, 2, 3, 4, 5]
);
// [4, 8] - only one iteration through the arrayPractical Utilities
Curry
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...more) => curried(...args, ...more);
};
}
const add = curry((a, b, c) => a + b + c);
add(1)(2)(3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6Debounce
function debounce(fn, ms) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), ms);
};
}Throttle
function throttle(fn, ms) {
let lastCall = 0;
return (...args) => {
const now = Date.now();
if (now - lastCall >= ms) {
lastCall = now;
return fn(...args);
}
};
}Once
function once(fn) {
let called = false;
let result;
return (...args) => {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}Best Practices
1. Prefer pure functions — Isolate side effects to edges of your application 2. Use immutable updates — Always return new data, never mutate 3. Compose small functions — Build complex behavior from simple parts 4. Use higher-order functions — map, filter, reduce instead of loops 5. Memoize expensive computations — Cache results of pure functions 6. Avoid shared mutable state — Pass data explicitly through functions 7. Make side effects explicit — Clearly mark functions with side effects 8. Use const by default — Prevent accidental reassignment
Concurrency Patterns
Sequential, parallel, batched execution, concurrency pools, retry with exponential backoff, timeout wrappers, async debounce, async throttle, for-await-of, async generators, stream chunking, AbortController cancellation, semaphore pattern.
Sequential Execution
// One at a time (traditional)
async function sequential(items) {
const results = [];
for (const item of items) {
results.push(await processItem(item));
}
return results;
}
// ES2025: Using Array.fromAsync with async generator
async function* processSequentially(items) {
for (const item of items) {
yield await processItem(item);
}
}
const results = await Array.fromAsync(processSequentially(items));Parallel Execution
// All at once
async function parallel(items) {
return Promise.all(items.map(item => processItem(item)));
}Batched Execution
// N at a time
async function batched(items, batchSize) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(item => processItem(item))
);
results.push(...batchResults);
}
return results;
}Concurrency Pool
async function pool(items, concurrency, fn) {
const results = [];
const executing = new Set();
for (const item of items) {
const promise = fn(item).then(result => {
executing.delete(promise);
return result;
});
results.push(promise);
executing.add(promise);
if (executing.size >= concurrency) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// Process 100 items, max 5 concurrent
await pool(items, 5, processItem);Retry Pattern
async function withRetry(fn, { retries = 3, delay = 1000, backoff = 2 } = {}) {
let lastError;
for (let attempt = 0; attempt < retries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt < retries - 1) {
await new Promise(r => setTimeout(r, delay * backoff ** attempt));
}
}
}
throw lastError;
}
const data = await withRetry(() => fetchData(), {
retries: 5,
delay: 500,
backoff: 2
});Timeout Wrapper
// ES2024: Using Promise.withResolvers()
function withTimeout(promise, ms, message = 'Timeout') {
const { promise: timeout, reject } = Promise.withResolvers();
setTimeout(() => reject(new Error(message)), ms);
return Promise.race([promise, timeout]);
}
const data = await withTimeout(fetchData(), 5000);Debounce Async
// ES2024: Using Promise.withResolvers()
function debounceAsync(fn, ms) {
let timeoutId;
let pending = null;
return (...args) => {
clearTimeout(timeoutId);
pending?.reject?.(new Error('Debounced'));
const { promise, resolve, reject } = Promise.withResolvers();
pending = { reject };
timeoutId = setTimeout(async () => {
try {
resolve(await fn(...args));
} catch (error) {
reject(error);
}
}, ms);
return promise;
};
}
const debouncedSearch = debounceAsync(searchAPI, 300);Throttle Async
function throttleAsync(fn, ms) {
let lastCall = 0;
let pending = null;
return async (...args) => {
const now = Date.now();
const timeSinceLastCall = now - lastCall;
if (timeSinceLastCall >= ms) {
lastCall = now;
return fn(...args);
}
if (!pending) {
pending = new Promise(resolve => {
setTimeout(async () => {
lastCall = Date.now();
pending = null;
resolve(await fn(...args));
}, ms - timeSinceLastCall);
});
}
return pending;
};
}Async Iteration
for-await-of
async function* fetchPages(url) {
let page = 1;
while (true) {
const response = await fetch(`${url}?page=${page}`);
const data = await response.json();
if (data.length === 0) break;
yield data;
page++;
}
}
for await (const page of fetchPages('/api/items')) {
processPage(page);
}Async Generators
async function* streamData(source) {
const reader = source.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield value;
}
} finally {
reader.releaseLock();
}
}Process Stream in Chunks
async function* chunkStream(stream, chunkSize) {
let buffer = [];
for await (const item of stream) {
buffer.push(item);
if (buffer.length >= chunkSize) {
yield buffer;
buffer = [];
}
}
if (buffer.length > 0) {
yield buffer;
}
}
for await (const chunk of chunkStream(dataStream, 100)) {
await processBatch(chunk);
}Cancellation Patterns
AbortController
async function fetchWithCancel(url, signal) {
const response = await fetch(url, { signal });
return response.json();
}
const controller = new AbortController();
// Start fetch
const promise = fetchWithCancel('/api/data', controller.signal);
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
const data = await promise;
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
}
}Cancellable Operations
function createCancellableOperation(fn) {
const controller = new AbortController();
const promise = (async () => {
try {
return await fn(controller.signal);
} catch (error) {
if (error.name === 'AbortError') {
return { cancelled: true };
}
throw error;
}
})();
return {
promise,
cancel: () => controller.abort()
};
}
const { promise, cancel } = createCancellableOperation(async (signal) => {
const response = await fetch('/api/data', { signal });
return response.json();
});
// Cancel if needed
cancel();Semaphore Pattern
class Semaphore {
#permits;
#queue = [];
constructor(permits) {
this.#permits = permits;
}
async acquire() {
if (this.#permits > 0) {
this.#permits--;
return;
}
const { promise, resolve } = Promise.withResolvers();
this.#queue.push(resolve);
return promise;
}
release() {
if (this.#queue.length > 0) {
const resolve = this.#queue.shift();
resolve();
} else {
this.#permits++;
}
}
async withPermit(fn) {
await this.acquire();
try {
return await fn();
} finally {
this.release();
}
}
}
// Limit concurrent operations to 3
const semaphore = new Semaphore(3);
await Promise.all(
items.map(item =>
semaphore.withPermit(() => processItem(item))
)
);ES2016 & ES2017 Features
Array.includes(), exponentiation operator, async/await, Object.values(), Object.entries(), String.padStart(), String.padEnd(), Object.getOwnPropertyDescriptors(), trailing commas in functions, SharedArrayBuffer, Atomics.
ES2016 (ES7)
The smallest ECMAScript release with just two features.
Array.prototype.includes()
Check if array contains a value (handles NaN correctly).
const arr = [1, 2, 3, NaN];
// ✅ includes() - handles NaN
arr.includes(2); // true
arr.includes(4); // false
arr.includes(NaN); // true
// ❌ indexOf() - doesn't find NaN
arr.indexOf(NaN); // -1 (wrong!)
// Optional start index
arr.includes(2, 2); // false (starts searching from index 2)Exponentiation Operator
// ❌ Before
Math.pow(2, 10); // 1024
// ✅ ES2016
2 ** 10; // 1024
// Right-associative
2 ** 3 ** 2; // 2 ** 9 = 512 (not 8 ** 2)
// Assignment operator
let x = 2;
x **= 3; // x = 8---
ES2017 (ES8)
Async Functions
Syntactic sugar over Promises for cleaner asynchronous code.
// ❌ Promise chains
function fetchUser(id) {
return fetch(`/api/users/${id}`)
.then(res => res.json())
.then(user => fetch(`/api/posts?userId=${user.id}`))
.then(res => res.json())
.catch(err => console.error(err));
}
// ✅ async/await
async function fetchUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
const postsRes = await fetch(`/api/posts?userId=${user.id}`);
return await postsRes.json();
} catch (err) {
console.error(err);
}
}
// Arrow function syntax
const fetchUser = async (id) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
};
// Async methods
const obj = {
async getData() {
return await fetchSomething();
}
};Object.values() and Object.entries()
const user = { name: 'Alice', age: 30, city: 'NYC' };
// Object.values() - get values array
Object.values(user); // ['Alice', 30, 'NYC']
// Object.entries() - get [key, value] pairs
Object.entries(user);
// [['name', 'Alice'], ['age', 30], ['city', 'NYC']]
// Iterate over entries
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}
// Transform object
const doubled = Object.fromEntries(
Object.entries(prices).map(([k, v]) => [k, v * 2])
);String Padding
// padStart - pad from beginning
'5'.padStart(3, '0'); // '005'
'42'.padStart(5, '*'); // '***42'
'hello'.padStart(10); // ' hello' (spaces)
// padEnd - pad from end
'5'.padEnd(3, '0'); // '500'
'hi'.padEnd(5, '.'); // 'hi...'
// Practical: Format numbers
const formatId = id => String(id).padStart(6, '0');
formatId(42); // '000042'
// Practical: Align columns
const items = [['Apple', 1.5], ['Banana', 0.75]];
for (const [name, price] of items) {
console.log(`${name.padEnd(10)}$${price.toFixed(2)}`);
}
// Apple $1.50
// Banana $0.75Object.getOwnPropertyDescriptors()
Get all property descriptors at once (useful for proper object cloning).
const source = {
name: 'Alice',
get fullName() { return this.name; }
};
// ❌ Object.assign loses getters/setters
const bad = Object.assign({}, source);
Object.getOwnPropertyDescriptor(bad, 'fullName');
// { value: 'Alice', writable: true, ... } <- Lost getter!
// ✅ Preserve descriptors
const good = Object.defineProperties(
{},
Object.getOwnPropertyDescriptors(source)
);
Object.getOwnPropertyDescriptor(good, 'fullName');
// { get: [Function], set: undefined, ... } <- Getter preserved!Trailing Commas in Function Parameters
// Now allowed (helps with cleaner diffs)
function greet(
name,
greeting, // trailing comma OK
) {
return `${greeting}, ${name}!`;
}
greet(
'Alice',
'Hello', // trailing comma OK
);SharedArrayBuffer and Atomics
Low-level multi-threading primitives (advanced).
// Create shared memory
const buffer = new SharedArrayBuffer(16);
const view = new Int32Array(buffer);
// Atomic operations (thread-safe)
Atomics.store(view, 0, 42);
Atomics.load(view, 0); // 42
Atomics.add(view, 0, 1); // Returns 42, view[0] is now 43
Atomics.compareExchange(view, 0, 43, 100); // If 43, set to 100ES2018 & ES2019 Features
Object rest/spread, for-await-of, Promise.finally(), RegExp named capture groups, lookbehind assertions, dotAll flag, Unicode property escapes, Array.flat(), Array.flatMap(), Object.fromEntries(), String.trimStart(), String.trimEnd(), optional catch binding, Symbol.description, stable sort.
ES2018 (ES9)
Rest/Spread Properties for Objects
// Object spread (copying)
const defaults = { theme: 'dark', lang: 'en' };
const userPrefs = { theme: 'light' };
const settings = { ...defaults, ...userPrefs };
// { theme: 'light', lang: 'en' }
// Object rest (destructuring)
const { name, ...rest } = { name: 'Alice', age: 30, city: 'NYC' };
// name = 'Alice'
// rest = { age: 30, city: 'NYC' }
// Function parameters
function update({ id, ...changes }) {
return { id, ...changes, updatedAt: new Date() };
}Asynchronous Iteration
// Async generator
async function* fetchPages(url) {
let page = 1;
while (true) {
const res = await fetch(`${url}?page=${page}`);
const data = await res.json();
if (data.length === 0) break;
yield data;
page++;
}
}
// for-await-of
for await (const page of fetchPages('/api/items')) {
processPage(page);
}
// Async iterables
const asyncIterable = {
[Symbol.asyncIterator]() {
let i = 0;
return {
async next() {
if (i >= 3) return { done: true };
await delay(100);
return { value: i++, done: false };
}
};
}
};Promise.prototype.finally()
Run cleanup code regardless of Promise outcome.
showSpinner();
fetch('/api/data')
.then(res => res.json())
.then(data => process(data))
.catch(err => showError(err))
.finally(() => {
hideSpinner(); // Always runs
});
// With async/await
async function fetchData() {
showSpinner();
try {
const res = await fetch('/api/data');
return await res.json();
} catch (err) {
showError(err);
} finally {
hideSpinner(); // Always runs
}
}RegExp Named Capture Groups
// ❌ Before: numbered groups
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const match = '2024-03-15'.match(dateRegex);
const year = match[1]; // Which is which?
const month = match[2];
const day = match[3];
// ✅ Named groups
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2024-03-15'.match(dateRegex);
const { year, month, day } = match.groups;
// year = '2024', month = '03', day = '15'
// In replace()
const result = '2024-03-15'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>'
);
// '03/15/2024'RegExp Lookbehind Assertions
// Lookahead (existed before): (?=...) and (?!...)
// Lookbehind (new): (?<=...) and (?<!...)
// Positive lookbehind: match if preceded by pattern
const priceRegex = /(?<=\$)\d+(\.\d{2})?/;
'$19.99'.match(priceRegex); // ['19.99']
'€19.99'.match(priceRegex); // null
// Negative lookbehind: match if NOT preceded by pattern
const notEuroRegex = /(?<!€)\d+(\.\d{2})?/;
'€19.99'.match(notEuroRegex); // null
'$19.99'.match(notEuroRegex); // ['19.99']RegExp dotAll Flag (s)
Make . match newlines too.
// ❌ Before: dot doesn't match newlines
/foo.bar/.test('foo\nbar'); // false
// ✅ With s flag
/foo.bar/s.test('foo\nbar'); // true
// Check flag
/foo.bar/s.dotAll; // trueRegExp Unicode Property Escapes
Match characters by Unicode property.
// Match Greek letters
/\p{Script=Greek}/u.test('π'); // true
/\p{Script=Greek}/u.test('p'); // false
// Match any letter
/\p{Letter}/u.test('Ω'); // true
/\p{Letter}/u.test('5'); // false
// Match emoji
/\p{Emoji}/u.test('😀'); // true
// General category properties
/\p{Lowercase}/u; // Lowercase letters
/\p{Uppercase}/u; // Uppercase letters
/\p{Alphabetic}/u; // Alphabetic characters
/\p{Number}/u; // Any number (Nd, Nl, No)
/\p{Decimal_Number}/u; // Decimal digits (0-9 in any script)
/\p{Punctuation}/u; // Punctuation marks
/\p{White_Space}/u; // Whitespace characters
/\p{Math}/u; // Mathematical symbols
// Script properties
/\p{Script=Latin}/u; // Latin script
/\p{Script=Greek}/u; // Greek script
/\p{Script=Cyrillic}/u; // Cyrillic script
/\p{Script=Han}/u; // Chinese characters
/\p{Script=Hiragana}/u; // Japanese Hiragana
/\p{Script=Katakana}/u; // Japanese Katakana
/\p{Script=Arabic}/u; // Arabic script
/\p{Script=Hebrew}/u; // Hebrew scriptTemplate Literal Revision
Allow invalid escape sequences in tagged templates.
// Before ES2018, this would throw SyntaxError
function latex(strings) {
return strings.raw[0];
}
latex`\unicode`; // Now works, returns '\\unicode'
// Cooked value is undefined for invalid escapes
function tag(strings) {
console.log(strings[0]); // undefined
console.log(strings.raw[0]); // '\\unicode'
}
tag`\unicode`;---
ES2019 (ES10)
Array.prototype.flat()
Flatten nested arrays.
// Default: flatten one level
[1, [2, [3, [4]]]].flat(); // [1, 2, [3, [4]]]
// Specify depth
[1, [2, [3, [4]]]].flat(2); // [1, 2, 3, [4]]
// Flatten completely
[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]
// Removes holes
[1, , 3, , 5].flat(); // [1, 3, 5]Array.prototype.flatMap()
Map then flatten (one level).
// Equivalent to .map().flat()
const sentences = ['Hello world', 'How are you'];
// ❌ map returns nested arrays
sentences.map(s => s.split(' '));
// [['Hello', 'world'], ['How', 'are', 'you']]
// ✅ flatMap flattens
sentences.flatMap(s => s.split(' '));
// ['Hello', 'world', 'How', 'are', 'you']
// Can also filter by returning empty array
const nums = [1, 2, 3, 4];
nums.flatMap(n => n % 2 ? [n] : []); // [1, 3]Object.fromEntries()
Create object from key-value pairs (inverse of Object.entries).
// From entries array
Object.fromEntries([['a', 1], ['b', 2]]);
// { a: 1, b: 2 }
// From Map
const map = new Map([['name', 'Alice'], ['age', 30]]);
Object.fromEntries(map);
// { name: 'Alice', age: 30 }
// Transform object
const prices = { apple: 1, banana: 2 };
const doubled = Object.fromEntries(
Object.entries(prices).map(([k, v]) => [k, v * 2])
);
// { apple: 2, banana: 4 }
// Filter object
const filtered = Object.fromEntries(
Object.entries(obj).filter(([k, v]) => v > 0)
);
// URL search params to object
const params = new URLSearchParams('name=Alice&age=30');
Object.fromEntries(params);
// { name: 'Alice', age: '30' }String.prototype.trimStart() and trimEnd()
const str = ' Hello World ';
str.trimStart(); // 'Hello World '
str.trimEnd(); // ' Hello World'
str.trim(); // 'Hello World'
// Aliases (for compatibility)
str.trimLeft(); // Same as trimStart()
str.trimRight(); // Same as trimEnd()Optional Catch Binding
Omit the error parameter if not needed.
// ❌ Before: always needed parameter
try {
JSON.parse(input);
} catch (e) { // 'e' is unused
return null;
}
// ✅ Now optional
try {
JSON.parse(input);
} catch {
return null;
}Symbol.prototype.description
Access symbol description directly.
const sym = Symbol('mySymbol');
// ❌ Before: had to parse toString()
sym.toString(); // 'Symbol(mySymbol)'
// ✅ Now: direct access
sym.description; // 'mySymbol'
Symbol().description; // undefined
Symbol('').description; // ''Stable Array.prototype.sort()
Sort is now guaranteed to be stable (equal elements maintain order).
const items = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 25 },
];
// Sort by age - Alice and Bob maintain relative order
// Note: Use .toSorted() in ES2023+ to avoid mutation
const sorted = items.toSorted((a, b) => a.age - b.age);
// [
// { name: 'Charlie', age: 25 },
// { name: 'Alice', age: 30 }, <- Alice before Bob (stable)
// { name: 'Bob', age: 30 },
// ]Well-formed JSON.stringify()
Properly encode lone surrogates as escape sequences.
// Before: could produce invalid Unicode
JSON.stringify('\uD800'); // '"\uD800"' (invalid)
// After: escape sequences for lone surrogates
JSON.stringify('\uD800'); // '"\\ud800"' (valid)Function.prototype.toString() Revision
Returns exact source code including whitespace and comments.
function /* comment */ foo() { }
foo.toString();
// 'function /* comment */ foo() { }'
// (Before, comments/whitespace might be stripped)ES2022 & ES2023 Features
.at() indexing, Object.hasOwn(), top-level await, private class fields, static blocks, error cause, RegExp match indices, .toSorted(), .toReversed(), .toSpliced(), .with(), .findLast(), .findLastIndex(), hashbang grammar, symbols as WeakMap keys.
ES2022 (ES13)
Array.prototype.at()
Access elements from the end with negative indices.
const arr = ['a', 'b', 'c', 'd', 'e'];
// Positive indices (same as bracket notation)
arr.at(0); // 'a'
arr.at(2); // 'c'
// Negative indices (from end)
arr.at(-1); // 'e' (last element)
arr.at(-2); // 'd' (second to last)
// Works on strings too
'hello'.at(-1); // 'o'
// Works on TypedArrays
new Uint8Array([1, 2, 3]).at(-1); // 3Migration pattern:
// ❌ Before
const last = arr[arr.length - 1];
const secondLast = arr[arr.length - 2];
// ✅ After
const last = arr.at(-1);
const secondLast = arr.at(-2);Object.hasOwn()
Safe alternative to hasOwnProperty.
const obj = { name: 'Alice' };
// ✅ Safe and concise
Object.hasOwn(obj, 'name'); // true
Object.hasOwn(obj, 'toString'); // false (inherited)
// Works with objects that don't have hasOwnProperty
const nullProto = Object.create(null);
nullProto.key = 'value';
Object.hasOwn(nullProto, 'key'); // true
// nullProto.hasOwnProperty('key') // TypeError!Why not hasOwnProperty:
// hasOwnProperty can be overwritten
const malicious = {
hasOwnProperty: () => false,
secret: 'data'
};
malicious.hasOwnProperty('secret'); // false (wrong!)
Object.hasOwn(malicious, 'secret'); // true (correct!)Top-Level Await
Use await at module top level without async wrapper.
// config.js
const response = await fetch('/api/config');
export const config = await response.json();
// db.js
import { config } from './config.js';
export const db = await connectDatabase(config.dbUrl);
// app.js
import { db } from './db.js'; // Waits for db to be readyUse cases:
- Loading configuration
- Initializing database connections
- Dynamic module loading
- Conditional imports
Class Private Fields and Methods
True encapsulation with # prefix.
class BankAccount {
#balance = 0; // Private field
#transactionHistory = [];
constructor(initial) {
this.#balance = initial;
}
// Private method
#logTransaction(type, amount) {
this.#transactionHistory.push({ type, amount, date: new Date() });
}
deposit(amount) {
this.#balance += amount;
this.#logTransaction('deposit', amount);
}
withdraw(amount) {
if (amount > this.#balance) throw new Error('Insufficient funds');
this.#balance -= amount;
this.#logTransaction('withdrawal', amount);
}
get balance() {
return this.#balance;
}
// Private field check
static isAccount(obj) {
return #balance in obj;
}
}
const account = new BankAccount(100);
account.deposit(50);
// account.#balance // SyntaxError: Private field
// account.#logTransaction() // SyntaxError: Private methodStatic Class Fields and Blocks
class Config {
static #instance;
static version = '1.0.0';
static features = [];
// Static initialization block
static {
console.log('Initializing Config class');
this.features = ['auth', 'logging'];
if (process.env.NODE_ENV === 'development') {
this.features.push('debug');
}
}
static getInstance() {
if (!this.#instance) {
this.#instance = new Config();
}
return this.#instance;
}
}Error Cause
Chain errors with context.
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user', {
cause: { status: response.status, userId }
});
}
return await response.json();
} catch (error) {
throw new Error('User data unavailable', { cause: error });
}
}
try {
await fetchUserData(123);
} catch (error) {
console.error(error.message); // 'User data unavailable'
console.error(error.cause.message); // 'Failed to fetch user'
console.error(error.cause.cause); // { status: 404, userId: 123 }
}RegExp Match Indices
Get start/end positions with /d flag.
const str = 'Hello World';
const regex = /(?<greeting>Hello) (?<subject>World)/d;
const match = str.match(regex);
console.log(match.indices);
// [[0, 11], [0, 5], [6, 11]]
// [fullMatch, greeting, subject]
console.log(match.indices.groups);
// { greeting: [0, 5], subject: [6, 11] }---
ES2023 (ES14)
Array Change-by-Copy Methods
New methods that return modified copies without mutating the original.
toSorted()
const numbers = [3, 1, 4, 1, 5];
// ❌ Mutates original
const sorted = numbers.sort((a, b) => a - b);
console.log(numbers); // [1, 1, 3, 4, 5] - mutated!
// ✅ Returns new array
const numbers2 = [3, 1, 4, 1, 5];
const sorted2 = numbers2.toSorted((a, b) => a - b);
console.log(numbers2); // [3, 1, 4, 1, 5] - unchanged
console.log(sorted2); // [1, 1, 3, 4, 5] - new arraytoReversed()
const arr = [1, 2, 3, 4, 5];
// ❌ Mutates original
const reversed = arr.reverse();
// ✅ Returns new array
const arr2 = [1, 2, 3, 4, 5];
const reversed2 = arr2.toReversed();
console.log(arr2); // [1, 2, 3, 4, 5] - unchanged
console.log(reversed2); // [5, 4, 3, 2, 1] - new arraytoSpliced()
const months = ['Jan', 'Mar', 'Apr'];
// ❌ Mutates original
months.splice(1, 0, 'Feb');
// ✅ Returns new array
const months2 = ['Jan', 'Mar', 'Apr'];
const withFeb = months2.toSpliced(1, 0, 'Feb');
console.log(months2); // ['Jan', 'Mar', 'Apr'] - unchanged
console.log(withFeb); // ['Jan', 'Feb', 'Mar', 'Apr'] - new array
// Remove elements
const withoutMar = months2.toSpliced(1, 1);
// ['Jan', 'Apr']with()
Replace element at index.
const colors = ['red', 'green', 'blue'];
// ❌ Mutates original
colors[1] = 'yellow';
// ✅ Returns new array
const colors2 = ['red', 'green', 'blue'];
const updated = colors2.with(1, 'yellow');
console.log(colors2); // ['red', 'green', 'blue'] - unchanged
console.log(updated); // ['red', 'yellow', 'blue'] - new array
// Supports negative indices
const last = colors2.with(-1, 'purple');
// ['red', 'green', 'purple']findLast() and findLastIndex()
Search from the end of the array.
const numbers = [1, 2, 3, 2, 1];
// Find last occurrence
const lastTwo = numbers.findLast(n => n === 2); // 2
const lastTwoIdx = numbers.findLastIndex(n => n === 2); // 3
// Practical example
const transactions = [
{ id: 1, type: 'credit' },
{ id: 2, type: 'debit' },
{ id: 3, type: 'credit' },
{ id: 4, type: 'debit' }
];
const lastCredit = transactions.findLast(t => t.type === 'credit');
// { id: 3, type: 'credit' }
const lastCreditIdx = transactions.findLastIndex(t => t.type === 'credit');
// 2Hashbang Grammar
Executable scripts with shebang.
#!/usr/bin/env node
// This is now valid JavaScript syntax
console.log('Hello from CLI!');Symbols as WeakMap Keys
Use symbols (except registered) as WeakMap keys.
const privateData = new WeakMap();
const key = Symbol('private');
const obj = {};
privateData.set(key, { secret: 'data' });
console.log(privateData.get(key)); // { secret: 'data' }
// Registered symbols still not allowed
const registered = Symbol.for('global');
// privateData.set(registered, 'value'); // TypeErrorPractical Migrations
Sorting Collections
// Before: Clone then sort
const sortedUsers = [...users].sort((a, b) => a.name.localeCompare(b.name));
// After: Single method
const sortedUsers = users.toSorted((a, b) => a.name.localeCompare(b.name));State Updates (React/Redux style)
// Before
function updateItem(items, index, newValue) {
const copy = [...items];
copy[index] = newValue;
return copy;
}
// After
function updateItem(items, index, newValue) {
return items.with(index, newValue);
}Finding Last Match
// Before
const logs = getLogEntries();
let lastError = null;
for (let i = logs.length - 1; i >= 0; i--) {
if (logs[i].level === 'error') {
lastError = logs[i];
break;
}
}
// After
const lastError = logs.findLast(entry => entry.level === 'error');ES2024 Features
Object.groupBy(), Map.groupBy(), Promise.withResolvers(), String.isWellFormed(), String.toWellFormed(), RegExp v flag (Unicode sets), resizable ArrayBuffers, Atomics.waitAsync().
Object.groupBy() and Map.groupBy()
Group array elements by a key.
const inventory = [
{ name: 'asparagus', type: 'vegetable', quantity: 5 },
{ name: 'banana', type: 'fruit', quantity: 0 },
{ name: 'goat', type: 'meat', quantity: 23 },
{ name: 'cherry', type: 'fruit', quantity: 5 },
{ name: 'fish', type: 'meat', quantity: 22 }
];
// Group by type
const byType = Object.groupBy(inventory, item => item.type);
/*
{
vegetable: [{ name: 'asparagus', ... }],
fruit: [{ name: 'banana', ... }, { name: 'cherry', ... }],
meat: [{ name: 'goat', ... }, { name: 'fish', ... }]
}
*/
// Group by availability
const byAvailability = Object.groupBy(inventory, item =>
item.quantity > 0 ? 'inStock' : 'outOfStock'
);
// Map.groupBy for object keys
const byTypeMap = Map.groupBy(inventory, item => item.type);
// Returns Map instead of plain objectMigration from manual grouping:
// ❌ Before
const grouped = items.reduce((acc, item) => {
const key = item.category;
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {});
// ✅ After
const grouped = Object.groupBy(items, item => item.category);When to use Map.groupBy:
// Use Map.groupBy when keys are non-strings
const byDate = Map.groupBy(events, event => event.date);
// Keys are Date objects, which work better as Map keys
// Or when key order matters
const byPriority = Map.groupBy(tasks, task => task.priority);
// Map preserves insertion orderPromise.withResolvers()
Create a Promise with externally accessible resolve/reject.
// ❌ Before: Awkward closure pattern
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// Use resolve/reject somewhere else
resolve('done');
// ✅ After: Clean destructuring
const { promise, resolve, reject } = Promise.withResolvers();
// Use anywhere
setTimeout(() => resolve('done'), 1000);
await promise; // 'done'Practical example: Event-to-Promise:
function waitForEvent(element, eventName) {
const { promise, resolve } = Promise.withResolvers();
element.addEventListener(eventName, resolve, { once: true });
return promise;
}
const button = document.querySelector('button');
await waitForEvent(button, 'click');
console.log('Button was clicked!');Practical example: Manual async control:
class AsyncQueue {
#queue = [];
#pending = null;
async next() {
if (this.#queue.length > 0) {
return this.#queue.shift();
}
this.#pending = Promise.withResolvers();
return this.#pending.promise;
}
push(item) {
if (this.#pending) {
this.#pending.resolve(item);
this.#pending = null;
} else {
this.#queue.push(item);
}
}
}Well-Formed Unicode Strings
Check and fix malformed UTF-16 strings.
// Check if string is well-formed
const valid = 'Hello 👋';
const invalid = 'Hi \uD800'; // Lone surrogate
valid.isWellFormed(); // true
invalid.isWellFormed(); // false
// Fix malformed strings
invalid.toWellFormed(); // 'Hi �' (replacement character)
// Use before encoding for URLs/APIs
function safeEncode(str) {
return encodeURIComponent(str.toWellFormed());
}RegExp v Flag (Unicode Sets)
Enhanced unicode handling in regex.
// Set operations in character classes
const emoji = /[\p{Emoji}--\p{ASCII}]/v; // Emoji minus ASCII
// String properties
const greekLetters = /\p{Script=Greek}/v;
// Intersection
const pattern = /[[a-z]&&[^aeiou]]/v; // Consonants only
// Nested character classes
const complex = /[[0-9]--[0-4]]/v; // 5-9 onlyResizable ArrayBuffers
// Create resizable buffer
const buffer = new ArrayBuffer(1024, { maxByteLength: 4096 });
console.log(buffer.byteLength); // 1024
console.log(buffer.maxByteLength); // 4096
console.log(buffer.resizable); // true
// Resize within bounds
buffer.resize(2048);
console.log(buffer.byteLength); // 2048
// Transfer ownership
const newBuffer = buffer.transfer(512);
console.log(buffer.byteLength); // 0 (detached)
console.log(newBuffer.byteLength); // 512Atomics.waitAsync()
Asynchronous shared memory waiting.
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);
// Async wait (doesn't block main thread)
const result = Atomics.waitAsync(sharedArray, 0, 0);
if (result.async) {
result.value.then(status => {
console.log('Woke up:', status); // 'ok' or 'timed-out'
});
}
// In another context/worker
Atomics.notify(sharedArray, 0);Browser/Node.js Support
| Feature | Chrome | Firefox | Safari | Node.js |
|---|---|---|---|---|
| Object.groupBy | 117+ | 119+ | 17.4+ | 21+ |
| Promise.withResolvers | 119+ | 121+ | 17.4+ | 22+ |
| String.isWellFormed | 111+ | 119+ | 16.4+ | 20+ |
| RegExp v flag | 112+ | 116+ | 17+ | 20+ |
| Resizable ArrayBuffer | 111+ | 111+ | 16.4+ | 20+ |
Check https://caniuse.com for current status.
ES2025 Features
Set operations, iterator helpers, explicit resource management, Array.fromAsync, Error.isError, Promise.try, Float16, Intl.DurationFormat, import attributes, RegExp.escape, pattern modifiers, duplicate named capture groups.
Set Methods
Mathematical set operations.
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// Union: elements in either set
setA.union(setB);
// Set {1, 2, 3, 4, 5, 6}
// Intersection: elements in both sets
setA.intersection(setB);
// Set {3, 4}
// Difference: elements in A but not B
setA.difference(setB);
// Set {1, 2}
// Symmetric difference: elements in either but not both
setA.symmetricDifference(setB);
// Set {1, 2, 5, 6}
// Subset check
const setC = new Set([1, 2]);
setC.isSubsetOf(setA); // true
setA.isSupersetOf(setC); // true
// Disjoint check (no common elements)
const setD = new Set([7, 8]);
setA.isDisjointFrom(setD); // truePractical example:
const userPermissions = new Set(['read', 'write']);
const requiredPermissions = new Set(['read', 'delete']);
// Check what's missing
const missing = requiredPermissions.difference(userPermissions);
// Set {'delete'}
// Check if has all required
const hasAll = requiredPermissions.isSubsetOf(userPermissions);
// falseIterator Helpers
Process iterators without converting to arrays.
// Create iterator
const numbers = [1, 2, 3, 4, 5].values();
// map() on iterator
const doubled = numbers.map(n => n * 2);
// Iterator yielding 2, 4, 6, 8, 10
// filter() on iterator
const evens = [1, 2, 3, 4, 5].values().filter(n => n % 2 === 0);
// Iterator yielding 2, 4
// take() - limit results
const firstThree = [1, 2, 3, 4, 5].values().take(3);
// Iterator yielding 1, 2, 3
// drop() - skip results
const afterTwo = [1, 2, 3, 4, 5].values().drop(2);
// Iterator yielding 3, 4, 5
// flatMap()
const nested = [[1, 2], [3, 4]].values().flatMap(arr => arr);
// Iterator yielding 1, 2, 3, 4
// reduce()
const sum = [1, 2, 3, 4, 5].values().reduce((a, b) => a + b, 0);
// 15
// toArray() - convert to array
const arr = [1, 2, 3].values().map(n => n * 2).toArray();
// [2, 4, 6]
// forEach()
[1, 2, 3].values().forEach(n => console.log(n));
// some() / every()
[1, 2, 3].values().some(n => n > 2); // true
[1, 2, 3].values().every(n => n > 0); // true
// find()
[1, 2, 3, 4].values().find(n => n > 2); // 3Lazy evaluation benefits:
// Process large datasets without loading all into memory
function* generateLargeDataset() {
for (let i = 0; i < 1000000; i++) {
yield { id: i, value: Math.random() };
}
}
// Only processes 10 items, not all 1M
const firstTen = generateLargeDataset()
.filter(item => item.value > 0.9)
.take(10)
.toArray();Iterator.from()
Create iterators from any iterable.
// Convert iterable to iterator with helpers
const setIterator = Iterator.from(new Set([1, 2, 3]));
// Now has all iterator helper methods
setIterator
.filter(n => n > 1)
.map(n => n * 2)
.toArray();
// [4, 6]
// Works with any iterable
const mapIter = Iterator.from(new Map([['a', 1], ['b', 2]]));
const stringIter = Iterator.from('hello');RegExp.escape()
Safely escape strings for use in regex.
const userInput = 'price: $100 (USD)';
// ❌ Before: Manual escaping (error-prone)
const escaped = userInput.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// ✅ After: Built-in escape
const escaped = RegExp.escape(userInput);
// 'price: \\$100 \\(USD\\)'
const pattern = new RegExp(escaped);
pattern.test('price: $100 (USD)'); // truePractical example: Safe search:
function createSearchPattern(searchTerm) {
return new RegExp(RegExp.escape(searchTerm), 'gi');
}
const query = 'C++ (language)';
const pattern = createSearchPattern(query);
// Safely matches "C++ (language)" in textRegExp Pattern Modifiers (Inline Flags)
Apply flags to specific parts of a regex instead of the whole pattern.
// Apply case-insensitivity only to part of pattern
const pattern = /^(?i:hello) world$/;
pattern.test('hello world'); // true
pattern.test('HELLO world'); // true
pattern.test('Hello World'); // false (World not modified)
// Multiple modifiers
const mixed = /(?i:abc)(?-i:DEF)/;
// abc is case-insensitive, DEF must be exact case
// Useful for partial matching
const filePattern = /(?i:readme)\.md$/;
filePattern.test('README.md'); // true
filePattern.test('ReadMe.md'); // true
filePattern.test('readme.MD'); // false (.md must be lowercase)Duplicate Named Capture Groups
Reuse capture group names in different alternatives.
// ❌ Before: Each name must be unique
const datePattern = /(?<year>\d{4})-(?<month>\d{2})|(?<month2>\d{2})\/(?<year2>\d{4})/;
// ✅ Now: Same names allowed in different alternatives
const datePattern = /(?<year>\d{4})-(?<month>\d{2})|(?<month>\d{2})\/(?<year>\d{4})/;
// Both formats work with same group names
'2024-03-15'.match(datePattern).groups;
// { year: '2024', month: '03' }
'03/15/2024'.match(datePattern).groups;
// { year: '2024', month: '03' } (same names!)Explicit Resource Management (using)
Automatic cleanup of resources like file handles, connections, and locks.
// Synchronous disposal with `using`
{
using file = openFile('data.txt');
// Work with file...
} // file[Symbol.dispose]() called automatically
// Asynchronous disposal with `await using`
{
await using db = await connectDatabase();
await db.query('SELECT * FROM users');
} // db[Symbol.asyncDispose]() awaited automatically
// Creating disposable resources
class FileHandle {
#handle;
constructor(path) {
this.#handle = fs.openSync(path);
}
read() { /* ... */ }
[Symbol.dispose]() {
fs.closeSync(this.#handle);
console.log('File closed');
}
}
// DisposableStack for multiple resources
{
using stack = new DisposableStack();
const file1 = stack.use(openFile('a.txt'));
const file2 = stack.use(openFile('b.txt'));
// Both disposed in reverse order when block exits
}
// AsyncDisposableStack for async cleanup
{
await using stack = new AsyncDisposableStack();
const conn1 = stack.use(await connect('db1'));
const conn2 = stack.use(await connect('db2'));
}Key concepts:
Symbol.dispose- Sync cleanup methodSymbol.asyncDispose- Async cleanup methodDisposableStack- Aggregate multiple disposablesAsyncDisposableStack- Async versionSuppressedError- Wraps errors during disposal
Array.fromAsync()
Create arrays from async iterables (async version of Array.from).
// From async generator
async function* fetchPages() {
yield await fetch('/page/1').then(r => r.json());
yield await fetch('/page/2').then(r => r.json());
yield await fetch('/page/3').then(r => r.json());
}
const pages = await Array.fromAsync(fetchPages());
// [page1Data, page2Data, page3Data]
// From any async iterable
const chunks = await Array.fromAsync(readableStream);
// With mapping function
const doubled = await Array.fromAsync(
asyncGenerator(),
async (x) => x * 2
);
// From sync iterable with promises
const results = await Array.fromAsync([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]);
// [1, 2, 3]vs Promise.all:
// Promise.all - fetches ALL upfront, then awaits all
const results = await Promise.all(urls.map(u => fetch(u)));
// Array.fromAsync - processes lazily, one at a time
const results = await Array.fromAsync(asyncGenerator());Error.isError()
Reliably check if a value is an Error across realms (iframes, workers, vm).
// Problem: instanceof fails across realms
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const err = new iframe.contentWindow.Error('oops');
err instanceof Error; // false (different realm!)
Error.isError(err); // true ✓
// Works for all Error types
Error.isError(new TypeError('bad')); // true
Error.isError(new RangeError('oops')); // true
Error.isError(new SyntaxError('fail')); // true
Error.isError({ message: 'fake' }); // false
Error.isError(null); // falsePromise.try()
Start a Promise chain that might throw.
// ❌ Before: Wrap in Promise.resolve or async
const result = await Promise.resolve().then(() => {
return riskyOperation(); // Might throw
});
// ✅ After: Direct wrapping
const result = await Promise.try(() => {
return riskyOperation(); // Throws are caught
});
// Handles both sync and async
Promise.try(() => syncFunction())
.then(result => console.log(result))
.catch(error => console.error(error));Import Attributes
Specify module type explicitly.
// Import JSON
import config from './config.json' with { type: 'json' };
// Import CSS (when supported)
import styles from './styles.css' with { type: 'css' };
// Dynamic import with attributes
const data = await import('./data.json', { with: { type: 'json' } });Float16 Support
Half-precision floating point.
// Float16Array
const f16 = new Float16Array([1.5, 2.5, 3.5]);
// Math.f16round()
Math.f16round(1.337); // Rounds to float16 precision
// DataView methods
const buffer = new ArrayBuffer(2);
const view = new DataView(buffer);
view.setFloat16(0, 1.5);
view.getFloat16(0); // 1.5Intl.DurationFormat
Format time durations with locale support.
const duration = { hours: 1, minutes: 46, seconds: 40 };
// Different styles
new Intl.DurationFormat('en', { style: 'long' }).format(duration);
// "1 hour, 46 minutes, 40 seconds"
new Intl.DurationFormat('en', { style: 'short' }).format(duration);
// "1 hr, 46 min, 40 sec"
new Intl.DurationFormat('en', { style: 'narrow' }).format(duration);
// "1h 46m 40s"
new Intl.DurationFormat('en', { style: 'digital' }).format(duration);
// "1:46:40"
// Localized
new Intl.DurationFormat('fr', { style: 'long' }).format(duration);
// "1 heure, 46 minutes et 40 secondes"
new Intl.DurationFormat('de', { style: 'long' }).format(duration);
// "1 Stunde, 46 Minuten und 40 Sekunden"Browser/Node.js Support
| Feature | Chrome | Firefox | Safari | Node.js |
|---|---|---|---|---|
| Set methods | 122+ | 127+ | 17+ | 22+ |
| Iterator helpers | 122+ | 131+ | 18+ | 22+ |
| Iterator.from | 122+ | 131+ | 18+ | 22+ |
| RegExp.escape | 136+ | - | - | 23+ |
| Explicit Resource Mgmt | 134+ | - | - | 22+ |
| Array.fromAsync | 121+ | 115+ | 16.4+ | 22+ |
| Error.isError | - | - | - | - |
| Promise.try | 128+ | 132+ | 18+ | 22+ |
| Float16 | 121+ | 129+ | - | 22+ |
| Intl.DurationFormat | 129+ | - | 16.4+ | - |
Check https://caniuse.com for current status.
Immutability and Pure Functions
Immutable array operations (spread, toSorted, toReversed, toSpliced, with), immutable object operations (spread, destructuring, structuredClone), pure function patterns, state update patterns for React/Redux.
Core Principles
1. Immutability: Never modify data in place 2. Pure functions: Same input always produces same output, no side effects 3. First-class functions: Functions as values, passed around and composed 4. Declarative style: Describe what, not how
Immutable Array Patterns
Array Operations
const numbers = [1, 2, 3, 4, 5];
// Add element
const withSix = [...numbers, 6];
// Prepend element
const withZero = [0, ...numbers];
// Remove element (by value)
const withoutThree = numbers.filter(n => n !== 3);
// Remove element (by index) - ES2023
const withoutSecond = numbers.toSpliced(1, 1);
// Update element (by index) - ES2023
const updated = numbers.with(2, 99);
// Transform element at index
const doubledAtTwo = numbers.with(2, numbers.at(2) * 2);
// ES2023: Non-mutating methods
const sorted = numbers.toSorted((a, b) => b - a);
const reversed = numbers.toReversed();Immutable Object Patterns
Object Operations
const user = { name: 'Alice', age: 30 };
// Add/update property
const updated = { ...user, age: 31 };
// Add nested property
const withAddress = {
...user,
address: { city: 'NYC' }
};
// Update nested property
const withNewCity = {
...user,
address: { ...user.address, city: 'LA' }
};
// Remove property
const { age, ...userWithoutAge } = user;
// Rename property
const { name: fullName, ...rest } = user;
const renamed = { fullName, ...rest };
// Conditional property
const maybeAdmin = {
...user,
...(isAdmin && { role: 'admin' })
};Deep Operations
// Deep clone (modern - preserves types, handles circular refs)
const clone = structuredClone(obj);
// Deep clone (legacy - loses functions, Dates become strings)
// const clone = JSON.parse(JSON.stringify(obj));
// Deep update helper
function updatePath(obj, path, value) {
const keys = path.split('.');
if (keys.length === 1) {
return { ...obj, [keys[0]]: value };
}
return {
...obj,
[keys[0]]: updatePath(obj[keys[0]], keys.slice(1).join('.'), value)
};
}
const updated = updatePath(state, 'user.profile.name', 'Bob');Pure Functions
Characteristics
// ✅ Pure: Deterministic, no side effects
function add(a, b) {
return a + b;
}
function formatUser(user) {
return {
displayName: `${user.firstName} ${user.lastName}`,
initials: `${user.firstName[0]}${user.lastName[0]}`
};
}
// ❌ Impure: Uses external state
let counter = 0;
function incrementCounter() {
counter++; // Side effect: modifies external state
return counter;
}
// ❌ Impure: Non-deterministic
function getRandomUser(users) {
return users[Math.floor(Math.random() * users.length)];
}
// ❌ Impure: Side effect
function saveUser(user) {
localStorage.setItem('user', JSON.stringify(user)); // Side effect
return user;
}Purifying Impure Functions
// Impure: Depends on Date
function isExpired(token) {
return token.expiresAt < Date.now(); // Non-deterministic
}
// Pure: Inject current time
function isExpired(token, now) {
return token.expiresAt < now;
}
isExpired(token, Date.now());
// Impure: Random + mutates
function shuffle(array) {
return array.sort(() => Math.random() - 0.5); // Mutates!
}
// Pure: Inject randomness + non-mutating (ES2023)
function shuffle(array, random = Math.random) {
return array.toSorted(() => random() - 0.5);
}
shuffle(items); // Random
shuffle(items, () => 0.5); // Deterministic for testsState Updates (React/Redux style)
Updating Arrays in State
// Add item
const addTodo = (todos, newTodo) => [...todos, newTodo];
// Remove item
const removeTodo = (todos, id) => todos.filter(t => t.id !== id);
// Update item
const updateTodo = (todos, id, updates) =>
todos.map(t => t.id === id ? { ...t, ...updates } : t);
// Toggle item
const toggleTodo = (todos, id) =>
todos.map(t => t.id === id ? { ...t, done: !t.done } : t);
// Reorder items
const moveTodo = (todos, fromIndex, toIndex) => {
const result = todos.toSpliced(fromIndex, 1);
return result.toSpliced(toIndex, 0, todos[fromIndex]);
};Updating Nested State
// Update deeply nested property
const updateNestedState = (state, userId, field, value) => ({
...state,
users: {
...state.users,
[userId]: {
...state.users[userId],
profile: {
...state.users[userId].profile,
[field]: value
}
}
}
});
// With helper function
const setIn = (obj, path, value) => {
const [head, ...rest] = path;
if (rest.length === 0) {
return { ...obj, [head]: value };
}
return {
...obj,
[head]: setIn(obj[head] ?? {}, rest, value)
};
};
const newState = setIn(state, ['users', 'u1', 'profile', 'name'], 'Alice');Best Practices
1. Use const by default — Prevent accidental reassignment 2. Prefer ES2023 methods — .toSorted(), .toReversed(), .with() 3. Use spread for shallow copies — { ...obj }, [...arr] 4. Use structuredClone for deep copies — Handles circular refs 5. Return new objects — Never mutate parameters 6. Extract side effects — Keep pure logic separate from I/O 7. Inject dependencies — Pass Date.now, Math.random as params for testing 8. Use optional chaining — obj?.nested?.value instead of guards
Promises and Async/Await
Promise creation, Promise.withResolvers(), async/await, try/catch, error-first returns, top-level await, Promise.all(), Promise.allSettled(), Promise.race(), Promise.any(), anti-patterns to avoid.
Promise Fundamentals
Creating Promises
// Basic Promise
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (success) {
resolve(result);
} else {
reject(new Error('Failed'));
}
}, 1000);
});
// ES2024: Promise.withResolvers()
const { promise, resolve, reject } = Promise.withResolvers();
// Control from outside
someEvent.on('complete', resolve);
someEvent.on('error', reject);
// Already resolved/rejected
const resolved = Promise.resolve(42);
const rejected = Promise.reject(new Error('Failed'));Promise Chaining
Prefer async/await (see below) for most cases. Use .then() for simple transforms or when you need the callback style.fetchUser(userId)
.then(user => fetchPosts(user.id))
.then(posts => processPosts(posts))
.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => cleanup());Async/Await
Basic Usage
async function getUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
} catch (error) {
console.error('Failed to get user data:', error);
throw error;
}
}Error Handling Patterns
// Try/catch
async function withTryCatch() {
try {
const result = await riskyOperation();
return result;
} catch (error) {
return defaultValue;
}
}
// Error-first return (Go-style)
async function withErrorReturn() {
try {
const result = await riskyOperation();
return [null, result];
} catch (error) {
return [error, null];
}
}
const [error, data] = await withErrorReturn();
if (error) handleError(error);
else processData(data);
// Wrapper utility
function to(promise) {
return promise
.then(data => [null, data])
.catch(error => [error, null]);
}
const [err, user] = await to(fetchUser(id));Top-Level Await (ES2022)
// In ES modules (not CommonJS)
const config = await loadConfig();
const db = await connectDatabase(config);
export { db };Promise Combinators
Promise.all()
Wait for all promises; fail if any fails.
// Parallel execution
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
// With error handling
try {
const results = await Promise.all([taskA(), taskB(), taskC()]);
} catch (error) {
// Any rejection cancels all
console.error('One task failed:', error);
}Promise.allSettled()
Wait for all; get status of each.
const results = await Promise.allSettled([
fetchFromPrimary(),
fetchFromBackup(),
fetchFromCache()
]);
const successes = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failures = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);Promise.race()
First to settle (resolve or reject) wins.
// Timeout pattern (ES2024)
async function fetchWithTimeout(url, ms) {
const { promise: timeout, reject } = Promise.withResolvers();
const timerId = setTimeout(() => reject(new Error('Timeout')), ms);
try {
return await Promise.race([fetch(url), timeout]);
} finally {
clearTimeout(timerId);
}
}
// First responder
const data = await Promise.race([
fetchFromServer1(),
fetchFromServer2()
]);Promise.any()
First to succeed wins; fails only if all fail.
// Fallback pattern
try {
const data = await Promise.any([
fetchFromPrimary(),
fetchFromSecondary(),
fetchFromTertiary()
]);
} catch (error) {
// AggregateError with all failures
console.error('All sources failed:', error.errors);
}Anti-Patterns
Unnecessary async
// ❌ Unnecessary wrapper
async function getUser(id) {
return await fetchUser(id);
}
// ✅ Just return the promise
function getUser(id) {
return fetchUser(id);
}Sequential when parallel is possible
// ❌ Sequential (slow)
const users = await fetchUsers();
const posts = await fetchPosts();
const comments = await fetchComments();
// ✅ Parallel (fast)
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);Forgetting error handling
// ❌ Unhandled rejection
async function process() {
const data = await fetchData(); // If this fails, crash
return transform(data);
}
// ✅ Handle errors
async function process() {
try {
const data = await fetchData();
return transform(data);
} catch (error) {
logger.error('Processing failed:', error);
throw error; // or return fallback
}
}Mixing callbacks and promises
// ❌ Callback hell in async function
async function mixed() {
fs.readFile('file.txt', (err, data) => {
// This doesn't work with await
});
}
// ✅ Promisify callbacks
import { promisify } from 'util';
const readFile = promisify(fs.readFile);
async function clean() {
const data = await readFile('file.txt');
return data;
}
// Or use fs.promises
import { readFile } from 'fs/promises';Creating promise inside loop
// ❌ Promises created but not awaited properly
async function bad() {
items.forEach(async item => {
await processItem(item); // This doesn't wait!
});
// Function returns before items are processed
}
// ✅ Use for...of for sequential
async function sequential() {
for (const item of items) {
await processItem(item);
}
}
// ✅ Use Promise.all for parallel
async function parallel() {
await Promise.all(items.map(item => processItem(item)));
}Upcoming JavaScript Features
Temporal API (PlainDate, PlainTime, PlainDateTime, ZonedDateTime, Duration), Decorators, Decorator Metadata.
Temporal API (Stage 3 - Requires Polyfill)
Note: Temporal is at TC39 Stage 3 and requires a polyfill until native browser support arrives.
Modern replacement for the broken Date object. Immutable, timezone-aware, and calendar-correct.
Why Temporal over Date?
// ❌ Date problems
const date = new Date('2024-03-10'); // Parsed as UTC? Local? Depends!
date.setMonth(1); // Mutates original
date.getMonth(); // 0-indexed (January = 0)
// ✅ Temporal - immutable, explicit, correct
const date = Temporal.PlainDate.from('2024-03-10');
const nextMonth = date.add({ months: 1 }); // Returns new instance
date.month; // 3 (March, 1-indexed!)Temporal Types
// Requires polyfill until native support
// PlainDate - date only, no time or timezone
const birthday = Temporal.PlainDate.from('1990-05-15');
const today = Temporal.Now.plainDateISO();
// PlainTime - time only
const meeting = Temporal.PlainTime.from('14:30:00');
// PlainDateTime - date + time, no timezone
const appointment = Temporal.PlainDateTime.from('2024-03-15T14:30:00');
// ZonedDateTime - full date/time with timezone (DST-aware!)
const flight = Temporal.ZonedDateTime.from(
'2024-03-15T14:30:00[America/New_York]'
);
// Instant - exact moment in time (like Unix timestamp)
const now = Temporal.Now.instant();
// Duration - length of time
const duration = Temporal.Duration.from({ hours: 2, minutes: 30 });Date Arithmetic
const date = Temporal.PlainDate.from('2024-01-31');
// Add months correctly (no overflow bugs!)
date.add({ months: 1 }); // 2024-02-29 (leap year aware)
// Subtract
date.subtract({ days: 15 });
// Compare
date1.equals(date2);
Temporal.PlainDate.compare(date1, date2); // -1, 0, or 1
// Difference
const diff = date1.until(date2);
diff.days; // Number of days betweenTimezone Handling
// Convert between timezones
const nyTime = Temporal.ZonedDateTime.from(
'2024-03-15T14:30:00[America/New_York]'
);
const londonTime = nyTime.withTimeZone('Europe/London');
// Handle DST transitions correctly
const beforeDST = Temporal.ZonedDateTime.from(
'2024-03-10T01:30:00[America/New_York]'
);
beforeDST.add({ hours: 2 }); // Correctly handles "spring forward"Migration Guide
| Use Case | Date | Temporal |
|---|---|---|
| Store timestamps | Date.now() | Temporal.Now.instant() |
| Display dates | new Date() | Temporal.Now.zonedDateTimeISO() |
| Birthdays, holidays | new Date(y, m-1, d) | Temporal.PlainDate.from() |
| Meeting times | Manual TZ conversion, DST bugs | Temporal.ZonedDateTime |
| Duration math | Manual calculation | Temporal.Duration |
---
Decorators (Stage 3 - Requires Transpiler)
Annotate and modify classes/methods with @decorator syntax. Requires Babel or TypeScript 5.0+.
// Requires transpiler (Babel or TypeScript 5.0+)
// Method decorator
function logged(target, context) {
return function (...args) {
console.log(`Calling ${context.name} with`, args);
return target.apply(this, args);
};
}
class Calculator {
@logged
add(a, b) {
return a + b;
}
}
// Class decorator
function singleton(Class, context) {
let instance;
return function (...args) {
if (!instance) {
instance = new Class(...args);
}
return instance;
};
}
@singleton
class Database {
constructor(url) {
this.url = url;
}
}Key points:
@decoratorsyntax before classes, methods, fields, accessors- Receives
(value, context)- value being decorated + metadata - Must return the decorated value (or replacement)
- No parameter decorators (unlike TypeScript legacy decorators)
- TypeScript 5.0+ supports TC39 decorators with
experimentalDecorators: false
Decorator Metadata (Stage 3 - Requires Transpiler)
Store metadata on decorated elements (complements Decorators proposal). Requires Babel or TypeScript 5.0+.
function meta(value) {
return function (target, context) {
context.metadata[context.name] = value;
return target;
};
}
class User {
@meta('string')
name;
@meta('number')
age;
}
User[Symbol.metadata];
// { name: 'string', age: 'number' }---
Feature Support Summary
| Feature | Status | Tooling |
|---|---|---|
| Temporal | Stage 3 | Requires polyfill |
| Decorators | Stage 3 | Babel, TypeScript 5.0+ |
| Decorator Metadata | Stage 3 | Babel, TypeScript 5.0+ |
Resources
- TC39 Proposals: https://github.com/tc39/proposals
- Can I Use: https://caniuse.com (browser support tables)