Stop Writing Polyfills — Modern JavaScript Has Already Solved That
Every year, TC39 ships features that make entire utility libraries obsolete. Most engineers don't notice until two years later when a teammate drops a one-liner that replaces 40 lines of lodash.
The last three spec editions — ES2024, ES2025, and ES2026 — have collectively added more practical, day-to-day features than the previous five years combined. This isn't a changelog — it's the features that will change how you write JavaScript daily.
ES2024: The Ones You Already Missed
ES2024 was ratified in June 2024. These features are fully supported across all modern browsers and Node.js. If you're not using them, you've been leaving cleaner code on the table for a year.
Promise.withResolvers(): Kill the Deferred Pattern
Every codebase has some version of this:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
It's the "deferred" pattern. It works. It's also ugly, error-prone, and requires leaking variables out of the constructor scope.
Now:
const { promise, resolve, reject } = Promise.withResolvers();
// Use anywhere
eventBus.on('data', resolve);
eventBus.on('error', reject);
const result = await promise;
One line. No variable hoisting tricks. This is especially useful for bridging callback-based APIs into async/await code — event emitters, WebSocket messages, postMessage handlers.
Browser support: Chrome 119+, Firefox 121+, Safari 17.2+, Node.js 22+.
Object.groupBy() and Map.groupBy(): Native Grouping
Grouping an array by a property used to require reduce boilerplate or reaching for lodash:
// Before — the reduce ritual
const grouped = items.reduce((acc, item) => {
const key = item.category;
(acc[key] ??= []).push(item);
return acc;
}, {});
// After — one line
const grouped = Object.groupBy(items, (item) => item.category);
// { electronics: [...], clothing: [...], books: [...] }
Need a Map instead of a plain object (for non-string keys or insertion order)?
const byStatus = Map.groupBy(users, (user) =>
user.age >= 18 ? 'adult' : 'minor'
);
This single feature eliminates one of the most common reasons teams still import lodash.
Browser support: Chrome 117+, Firefox 119+, Safari 17.2+, Node.js 21+.
Well-Formed Unicode Strings
JavaScript strings can contain lone surrogates — half of a Unicode pair that makes string operations unpredictable. ES2024 added two methods to detect and fix this:
const messy = 'hello\uD800world'; // lone surrogate
messy.isWellFormed(); // false
messy.toWellFormed(); // 'hello�world' — replaces lone surrogates with U+FFFD
Essential if you're passing user-generated strings to encodeURIComponent, TextEncoder, or any
API that rejects malformed UTF-16.
Browser support: Chrome 111+, Firefox 119+, Safari 16.4+.
ArrayBuffer.prototype.transfer()
Move ownership of an ArrayBuffer to a new one, detaching the original. Think Rust's move semantics for binary data:
const original = new ArrayBuffer(1024);
const transferred = original.transfer();
console.log(original.byteLength); // 0 — detached
console.log(transferred.byteLength); // 1024
// Resize during transfer
const resized = new ArrayBuffer(512).transfer(1024); // now 1024 bytes
Useful for zero-copy handoffs between workers, WASM modules, and streaming pipelines.
Browser support: Chrome 114+, Firefox 122+, Safari 17.4+.
ES2025: Shipped Last Year, Still Underused
ES2025 was ratified in June 2025. These features are finalized and widely supported — yet most codebases haven't adopted them.
Iterator Helpers: The End of Throwaway Arrays
Before ES2025, processing a sequence meant creating intermediate arrays at every step:
const results = data
.filter((x) => x.active)
.map((x) => x.name)
.slice(0, 10);
Three arrays created. Three full passes over the data. For 10 results.
Now:
const results = data
.values()
.filter((x) => x.active)
.map((x) => x.name)
.take(10)
.toArray();
Lazy evaluation. The pipeline stops after finding 10 matches. No intermediate arrays. On a dataset of 100K items where only 15 match your filter, the old approach processes every element three times. The new approach stops at 15.
This matters even more with generators and infinite sequences:
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// First 5 even Fibonacci numbers — lazily evaluated
const evenFibs = fibonacci()
.filter((n) => n % 2 === 0)
.take(5)
.toArray();
// [0, 2, 8, 34, 144]
Try doing that cleanly with Array.prototype methods. You can't — they don't work on iterators.
The full method set includes .map(), .filter(), .flatMap(), .reduce(), .forEach(),
.some(), .every(), .find(), .take(), .drop(), and .toArray(). Plus Iterator.from()
to convert any iterable.
Browser support: Chrome 122+, Firefox 131+, Safari 18+.
Set Methods: Mathematical Set Operations, Finally
For years, computing the intersection of two sets in JavaScript looked like this:
const intersection = new Set([...a].filter((x) => b.has(x)));
Spread into array, filter, wrap in a new Set. Embarrassing for a language used by millions.
ES2025 fixes this with proper set algebra:
const frontend = new Set(['Alice', 'Bob', 'Charlie']);
const backend = new Set(['Bob', 'Diana', 'Charlie']);
frontend.union(backend); // Set {'Alice', 'Bob', 'Charlie', 'Diana'}
frontend.intersection(backend); // Set {'Bob', 'Charlie'}
frontend.difference(backend); // Set {'Alice'}
frontend.symmetricDifference(backend); // Set {'Alice', 'Diana'}
frontend.isSubsetOf(backend); // false
frontend.isDisjointFrom(backend); // false
Seven methods that eliminate an entire class of utility functions. If you're maintaining a setUtils
file somewhere, you can delete it.
Real-world use: Permission systems, tag filtering, feature flag comparisons — anywhere you compare groups of things.
Browser support: Chrome 122+, Firefox 127+, Safari 17+.
Promise.try(): Unified Error Handling
Sync and async errors behave differently in JavaScript. A synchronous throw inside a Promise
constructor callback is caught, but a throw before the first await in an async function
propagates differently. Promise.try normalizes this:
// Before: awkward wrapping
function getData(id) {
return new Promise((resolve, reject) => {
try {
if (!id) throw new Error('ID required');
resolve(fetchFromDB(id));
} catch (e) {
reject(e);
}
});
}
// After: clean and consistent
function getData(id) {
return Promise.try(() => {
if (!id) throw new Error('ID required');
return fetchFromDB(id); // works whether sync or async
});
}
Synchronous errors are caught as rejections automatically. No more wrapping everything in
new Promise() just to get consistent error handling.
Browser support: All modern browsers and Node.js 22+.
Import Attributes: Secure JSON Imports
Importing JSON used to require a fetch call or a bundler plugin. Now it's part of the language:
import config from './config.json' with { type: 'json' };
import translations from './i18n/en.json' with { type: 'json' };
// Dynamic import with attributes
const data = await import('./data.json', { with: { type: 'json' } });
The with { type: 'json' } clause isn't optional decoration — it's a security measure. It tells the
runtime to only accept JSON, preventing a compromised server from swapping in executable
JavaScript.
Browser support: Chrome 123+, Safari 17.2+.
ES2026: The Big One
ES2026 is on track for ratification mid-2026, and it includes what might be the single most
impactful addition to JavaScript since async/await. Many of these features have already reached
Stage 4 and are shipping in browsers.
Temporal: The Date API We Deserved 25 Years Ago
Date is broken. Everyone knows it. Month-indexing starts at 0. Timezones are a black box.
Mutation is the default. Libraries like Moment, date-fns, and Luxon exist solely because the
built-in API is hostile.
Temporal replaces all of it:
// Current date and time, timezone-aware
const now = Temporal.Now.zonedDateTimeISO();
// "2026-05-23T17:45:00+05:30[Asia/Kolkata]"
// Plain dates — no month-index nonsense
const birthday = Temporal.PlainDate.from('1990-07-04');
const today = Temporal.Now.plainDateISO();
const age = today.since(birthday, { largestUnit: 'year' });
console.log(`Age: ${age.years} years`);
// Timezone conversions without tears
const meeting = Temporal.ZonedDateTime.from('2026-06-01T14:30:00[America/New_York]');
const inTokyo = meeting.withTimeZone('Asia/Tokyo');
// Automatically converts — no manual offset math
// Duration arithmetic that actually works
const deadline = Temporal.PlainDate.from('2026-12-31');
const remaining = Temporal.Now.plainDateISO().until(deadline);
console.log(`${remaining.days} days remaining`);
// Immutable — add returns a NEW object
const nextWeek = today.add({ days: 7 });
Everything is immutable. Timezone math is explicit. Calendar systems are pluggable. No more 0-based
months. No more new Date() giving you a mutable object that any function can silently modify.
This is the single most impactful proposal in years.
Browser support: Chrome 144+, Firefox 139+, Edge 144+. Polyfill available via @js-temporal/polyfill.
Explicit Resource Management: using and await using
If you've written Go, C#, or Python, this will feel familiar. If you've written JavaScript, you've written this exact pattern a thousand times:
const handle = openConnection();
try {
await doWork(handle);
} finally {
handle.close();
}
With explicit resource management:
async function processData() {
await using db = await connectToDatabase();
await using tx = await db.beginTransaction();
await tx.execute('INSERT INTO logs VALUES (?)', [entry]);
} // tx disposed first, then db — reverse order, automatically
Resources are cleaned up deterministically when the block exits — whether normally or via exception.
No finally blocks. No forgotten cleanup paths. No resource leaks.
Making your own resources disposable is straightforward:
class TempFile {
#path;
constructor(path) {
this.#path = path;
fs.writeFileSync(path, '');
}
[Symbol.dispose]() {
fs.unlinkSync(this.#path);
}
}
function processWithTemp() {
using tmp = new TempFile('/tmp/work.dat');
// ... use the file
} // automatically deleted here
For managing multiple dynamic resources, there's DisposableStack:
function processMultipleFiles(paths) {
using stack = new DisposableStack();
const files = paths.map((p) => stack.use(openFile(p)));
// All files cleaned up when stack is disposed
}
Browser support: Chrome 134+, Edge 134+. TypeScript 5.2+ already supports the syntax.
Array.fromAsync(): Collect Async Results in One Line
Collecting results from async iterables used to require manual loops:
// Before
const results = [];
for await (const item of asyncSource) {
results.push(item);
}
// After
const results = await Array.fromAsync(asyncSource);
Works with async generators, iterables of Promises, and supports a mapping function:
async function* fetchPages() {
for (let page = 1; page <= 3; page++) {
const res = await fetch(`/api/items?page=${page}`);
yield await res.json();
}
}
const allPages = await Array.fromAsync(fetchPages());
Browser support: Chrome 121+, Firefox 119+, Safari 17.5+.
Math.sumPrecise(): Floating-Point Done Right
// The classic JavaScript meme
0.1 + 0.2; // 0.30000000000000004
// Summing an array compounds the error
[0.1, 0.2, 0.3].reduce((a, b) => a + b); // 0.6000000000000001
// Fixed
Math.sumPrecise([0.1, 0.2, 0.3]); // 0.6
Uses a compensated summation algorithm internally. If you're doing financial calculations, analytics, or any domain where precision matters — this replaces an entire category of workarounds.
Browser support: Chrome 131+.
Map.getOrInsert(): Kill the Has-Then-Set Pattern
The "check if exists, insert if not, return value" pattern is everywhere:
// Before — verbose and wasteful (two lookups)
if (!cache.has(key)) {
cache.set(key, computeExpensiveValue(key));
}
const value = cache.get(key);
// After — one lookup, atomic
const value = cache.getOrInsert(key, defaultValue);
// With lazy computation — only called if key is missing
const value = cache.getOrInsertComputed(key, (k) => computeExpensiveValue(k));
The real win is for grouping patterns:
const groups = new Map();
for (const item of items) {
groups.getOrInsert(item.category, []).push(item);
}
One line instead of five. Works on WeakMap too.
Browser support: Available in modern browsers.
Uint8Array Base64 and Hex Methods
Base64 encoding previously required btoa/atob (which only work with strings) or third-party
libraries. Now it works directly with binary data:
const bytes = new Uint8Array([72, 101, 108, 108, 111]);
bytes.toBase64(); // "SGVsbG8="
bytes.toHex(); // "48656c6c6f"
Uint8Array.fromBase64('SGVsbG8='); // Uint8Array [72, 101, 108, 108, 111]
Uint8Array.fromHex('48656c6c6f'); // Uint8Array [72, 101, 108, 108, 111]
No more converting between strings and binary just to encode. If you're working with crypto, file uploads, or any binary protocol — this is a welcome simplification.
Browser support: Available in modern browsers.
Iterator.concat() and Error.isError()
Iterator.concat() — concatenate heterogeneous iterables lazily:
const combined = Iterator.concat(
[1, 2, 3],
new Set([4, 5]),
(function* () {
yield 6;
yield 7;
})()
);
[...combined]; // [1, 2, 3, 4, 5, 6, 7]
Error.isError() — reliable cross-realm error detection:
// instanceof breaks across iframes
iframeError instanceof Error; // false (different realm)
// Error.isError works everywhere
Error.isError(iframeError); // true
Error.isError({ message: 'fake', stack: 'fake' }); // false
What's NOT Coming (Despite the Hype)
A few high-profile proposals need a reality check:
| Proposal | Status | Reality |
|---|---|---|
| Record & Tuple | ❌ Withdrawn | Withdrawn April 2025. Not happening as proposed. |
| Pattern Matching | Stage 1 | Still in early design. Years away, if ever. |
| Pipeline Operator | Stage 2 | Stalled — syntax disagreements for years. |
| Decorators | Stage 3 | Spec-complete but not in ES2025 or ES2026. Use via transpilers. |
Don't plan your architecture around these. Use TypeScript equivalents or userland libraries in the meantime.
The Practical Takeaway
Three years of spec editions. Here's what to adopt right now:
Use today (ES2024 + ES2025 — full browser support):
- Object.groupBy — delete your lodash
groupByimport - Promise.withResolvers — cleaner callback-to-promise bridges
- Iterator helpers — stop creating intermediate arrays
- Set methods — delete your set utility functions
- Promise.try — unified sync/async error handling
- Import attributes — secure, native JSON imports
Start learning now (ES2026 — shipping in browsers):
- Temporal — browsers are shipping it. Start learning the API and polyfill if you're adventurous
usingdeclarations — TypeScript already supports them, the pattern is worth adopting today- Map.getOrInsert — simplify every caching and grouping pattern in your codebase
- Math.sumPrecise — if you do any numeric work, this is a free precision upgrade
The JavaScript spec isn't just evolving — it's absorbing the best ideas from the ecosystem and making them standard. The utility libraries you depend on today are the built-ins of tomorrow.