ES2025 Features You Should Actually Use
Learn about eight features you must adapt today in your production codebase. (9 min)
Share this post & I’ll send you some rewards for the referrals.
ES2025 is finalized. You’ve probably seen the listicles.
Here’s what’s different about this one: I took features from the spec and ran them against code I’ve written in the past year.
Some are genuinely useful. Others are niche enough that you’ll forget they exist in a month.
Let’s separate what matters from what doesn’t.
The tax you pay to run multiple agents. Cline Kanban (Partner)
If you’ve spent any time with coding agents, you know the feeling. You start the morning with a clean plan. Spin up a few agents. One is refactoring the auth module. Another is writing tests. A third is scaffolding a new API endpoint. You’re flying.
Then, around 10:30 AM, you look up and realize you have 20 terminal windows open. One agent is blocked waiting for a decision you forgot to make. Another finished 40 minutes ago, and you never noticed. A third went sideways three commits back. You’re no longer flying. You’re drowning.
You’ve shifted from human as driver to human as director. When running coding agents in parallel, the bottleneck isn’t just context. It’s your own attention trying to manage 10 agents across 10 terminals. You’re losing your mind to terminal chaos.
Meet Cline Kanban, a CLI-agnostic visual orchestration layer that makes multi-agent workflows usable across providers. Multiple agents, one UI. It’s the air traffic controller for the agents you’re already running, regardless of where they live.
Interoperable: Claude Code and Codex compatible, with more coming soon.
Full Visibility: Confidently run multiple agents and work through your backlog faster.
Smart Triage: See which agents are blocked or in review and jump in to unblock them.
Chain Tasks: Set dependencies so Agent B won’t start until Agent A is complete.
Familiar UI: Everything in a single Kanban view.
Stop tracking agents and start directing them. Get a meaningful edge with the beta release.
Install Cline Kanban Today: npm i -g cline
(Thanks to Cline Kanban for partnering on this post.)
1. Set Methods
Sets have been half-baked since ES6.
You could create them, add to them, and check membership.
However, if you wanted to intersect, union, or diff two sets, you had to write it yourself or reach for lodash.
Not anymore.
Before:
function intersect<T>(a: Set<T>, b: Set<T>): Set<T> {
const result = new Set<T>();
for (const item of a) {
if (b.has(item)) result.add(item);
}
return result;
}
const allowedRoles = new Set(['admin', 'editor', 'viewer']);
const userRoles = new Set(['editor', 'commenter']);
const effectiveRoles = intersect(allowedRoles, userRoles);After:
const allowedRoles = new Set(['admin', 'editor', 'viewer']);
const userRoles = new Set(['editor', 'commenter']);
const effectiveRoles = allowedRoles.intersection(userRoles);
// Set {'editor'}The full list: .intersection(), .union(), .difference(), .symmetricDifference(), .isSubsetOf(), .isSupersetOf(), .isDisjointFrom().
These are immutable. They return new Sets. That’s the right call.
If you’re doing any kind of permissions, feature flags, or tag filtering, this cleans up real code today. TypeScript support landed in 5.5.
2. Promise.withResolvers(), Externalized Control
Ever needed to create a promise and resolve it from outside the executor callback?
You’ve probably written this pattern dozens of times:
Before:
let resolve: (value: string) => void;
let reject: (reason: Error) => void;
const promise = new Promise<string>((res, rej) => {
resolve = res;
reject = rej;
});
// Later, somewhere else...
eventEmitter.on('data', (data) => resolve(data));
eventEmitter.on('error', (err) => reject(err));After:
const { promise, resolve, reject } = Promise.withResolvers<string>();
eventEmitter.on('data', (data) => resolve(data));
eventEmitter.on('error', (err) => reject(err));No more dangling let declarations. No more praying that resolve was assigned before someone calls it.
Any time you’re bridging between callback/event-based code and promises, like WebSocket message handlers, test utilities, and manual coordination. This approach is cleaner.
TypeScript has supported it since 5.4.
3. Iterator.prototype Methods, Lazy Pipeline Operations
Arrays have had .map(), .filter(), .reduce() forever.
But iterators, the things you get from generators, Map.keys(), Set.values(), had nothing. You’d spread them into arrays just to use basic operations.
Before:
function* generateUsers(): Generator<User> {
// yields users from a paginated API
}
// Wasteful: materializes entire iterator into memory
const activeEmails = [...generateUsers()]
.filter(user => user.isActive)
.map(user => user.email)
.slice(0, 10);After:
const activeEmails = generateUsers()
.filter(user => user.isActive)
.map(user => user.email)
.take(10)
.toArray();The key difference: the new version is lazy. It only processes elements as needed. When .take(10) has enough, it stops. No intermediate arrays. No wasted work.
Available methods: .map(), .filter(), .take(), .drop(), .flatMap(), .reduce(), .toArray(), .forEach(), .some(), .every(), .find().
If you work with generators, streams, or any large datasets where you don’t want to materialize everything into an array first, this is a significant improvement.
TypeScript support: 5.6+.
4. RegExp.escape(), The One We’ve Been Waiting 15 Years For
If you’ve ever built a regex from user input, you’ve either used a library or written a janky escape function that probably misses edge cases.
Before:
// Did you remember all the special chars?
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const searchTerm = 'price: $5.00 (USD)';
const pattern = new RegExp(escapeRegex(searchTerm), 'i');After:
const searchTerm = 'price: $5.00 (USD)';
const pattern = new RegExp(RegExp.escape(searchTerm), 'i');It’s a small thing, but it removes a class of bugs.
Any time you’re building dynamic regex patterns from external input (search, filtering, highlighting), use this.
5. Float16Array, Typed Array for ML and Graphics
A new typed array for 16-bit floating point numbers. Half the memory of Float32Array.
const weights = new Float16Array([0.5, -0.3, 0.8, 0.1]);
// Half the memory of Float32Array for the same dataIf you’re doing ML inference in the browser, WebGPU work, or processing large numeric datasets where memory matters more than precision, this is useful.
For everyone else, you’ll never touch it.
But it’s good that the platform supports it. This means less reason to drop to WASM for numeric work.
6. using / await using, Automatic Cleanup
If you’ve ever forgotten to close a file handle, release a database connection, or clear a timer, this one’s for you.
Explicit Resource Management adds using and await using declarations that automatically call a cleanup function when the variable goes out of scope.
Think Python’s with statement or C#’s using. JavaScript finally has it.
Before:
async function exportReport(reportId: string) {
const connection = await db.connect();
try {
const cursor = await connection.query(`SELECT * FROM reports WHERE id = $1`, [reportId]);
try {
const file = await fs.open('./report.csv', 'w');
try {
for await (const row of cursor) {
await file.write(formatCsvRow(row));
}
} finally {
await file.close();
}
} finally {
await cursor.close();
}
} finally {
await connection.release();
}
}After:
async function exportReport(reportId: string) {
await using connection = await db.connect();
await using cursor = await connection.query(`SELECT * FROM reports WHERE id = $1`, [reportId]);
await using file = await fs.open('./report.csv', 'w');
for await (const row of cursor) {
await file.write(formatCsvRow(row));
}
// connection, cursor, and file are all released automatically
}The nested try/finally pyramid is gone. Each resource cleans itself up when the block exits, whether it completes normally or throws.
For this to work, the resource needs a [Symbol.asyncDispose]() method (or [Symbol.dispose]() for synchronous resources).
Node.js built-ins like file handles already support it.
For your own classes, it’s straightforward:
class DatabaseConnection {
async [Symbol.asyncDispose]() {
await this.release();
}
}V8 and Node.js 22+ ship it. Browser support is still catching up: Chrome has it, Firefox and Safari are behind.
TypeScript supports it in 5.2+.
If you’re writing server-side code, start using this today. For client-side, wait a bit longer.
7. Promise.try(), Safe Function Wrapping
You have a function. It might be sync. It might be async. You want to call it and always get a Promise back, with errors caught either way.
Before:
function runTask(task: () => unknown): Promise<unknown> {
try {
const result = task();
return Promise.resolve(result);
} catch (err) {
return Promise.reject(err);
}
}Or the shorter-but-subtle version:
// This doesn't catch synchronous throws from task()
const result = Promise.resolve().then(() => task());After:
const result = Promise.try(task);One line. Catches sync throws. Wraps sync return values. Passes through Promises. Does the right thing every time.
This shows up anywhere you accept a callback that could be sync or async, like plugin systems, middleware chains, test runners, task queues.
It’s the Promise.withResolvers of error handling: a small API that eliminates a whole class of subtle bugs.
Supported in Chrome 128+, Firefox 132+, Safari 18.2+, Node.js 22+. TypeScript 5.7+.
8. JSON modules and import attributes
Import attributes (the with syntax) let you specify how an import should be interpreted:
import config from './config.json' with { type: 'json' };
import styles from './styles.css' with { type: 'css' };This has been available in bundlers for a while, but now it’s part of the language spec, and runtimes have caught up.
Chrome, Edge, Firefox, and Safari all support with { type: 'json' } as of April 2025. Node.js 22+ supports it too.
The type attribute isn’t an optional decoration but a security mechanism.
The runtime validates the MIME type before processing the file. If someone swaps your JSON endpoint for executable code, the import fails instead of running it.
One caveat: JSON modules only expose a default export. The entire JSON object comes in as default. No named exports.
If you’re importing JSON configs, fixtures, or static data, drop the fs.readFileSync / require() and use this.
TypeScript supports the syntax in 5.3+.
Other Good To Know Features
Three more features made it into the spec.
They’re niche enough that most developers won’t use them directly, but worth knowing they exist:
Error.isError(): returnstrueif the value is an Error, even across realms. If you’ve ever hadinstanceof Errorreturnfalsebecause the error came from a different iframe or Node.jsvmcontext, this fixes it.Regex pattern modifiers (
(?i:HELLO)): toggle flags likei,m,sfor specific parts of a regex instead of the whole pattern. Handy when one section of your regex needs case-insensitive matching but the rest doesn’t.Duplicate named capture groups: you can now reuse the same group name in different regex alternatives:
/(?<id>\d+)|(?<id>[a-f]+)/v. Previously, this was a syntax error, even though only one alternative can match at a time.
📌 TL;DR
Set methods replace the utility functions you’ve been copy-pasting for years/
Promise.withResolvers()externalizes resolve/reject, so no more danglingletdeclarations when bridging callbacks to promises.Iterator helpers process data lazily without spreading into arrays first.
RegExp.escape()safely escapes user input for dynamic regex patterns. Small API, eliminates a whole class of bugs.using/await usingauto-cleans resources (file handles, DB connections) when they go out of scope. The biggest quality-of-life feature in the spec.Promise.try()wraps any function (sync or async) and guarantees a Promise back with errors caught.Import attributes (
with { type: 'json' }) are now Baseline across all browsers and Node.js 22+.Float16Array is the only niche one. ML/graphics use cases only.
Minimum setup: TypeScript 5.6+, Node.js 22+. Most features are Baseline in browsers already.
Follow me on LinkedIn | Twitter(X) | Threads
Thank you for supporting this newsletter.
Consider sharing this post with your friends and get rewards.
You are the best! 🙏




