The T-Shaped Dev

ES2025 Features You Should Actually Use

PI

Petar Ivanov

Apr 22, 2026

6 min read

ES2025 Features You Should Actually Use

Source: The T-Shaped Dev · Author: Petar Ivanov · Date: 2026-04-22 · Original

ES2025 is finalized. Instead of another exhaustive listicle, Petar took the spec and tested each feature against code he'd actually written in the last year. Some genuinely improve daily work; a few are niche enough you'll forget they exist. Here's the working developer's cut — eight features worth adopting now, plus three "good to know" ones.


1. Set Methods — finally complete

Sets have been half-baked since ES6. You could create them, add items, and check membership, but if you wanted to intersect, union, or diff two sets, you wrote it yourself or reached for lodash.

Before — a hand-rolled helper you've probably copy-pasted across projects:

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 effectiveRoles = allowedRoles.intersection(userRoles);
// Set {'editor'}

Full list: .intersection(), .union(), .difference(), .symmetricDifference(), .isSubsetOf(), .isSupersetOf(), .isDisjointFrom().

These are immutable — they return new Sets rather than mutating the receiver, which is the right call for predictable code. If you do 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 when bridging events into promises:

Before:

let resolve: (value: string) => void;
let reject: (reason: Error) => void;

const promise = new Promise<string>((res, rej) => {
  resolve = res;
  reject = rej;
});

eventEmitter.on('data', (data) => resolve(data));
eventEmitter.on('error', (err) => reject(err));

That dangling let is ugly, and TypeScript has to be convinced resolve is assigned before use.

After:

const { promise, resolve, reject } = Promise.withResolvers<string>();

eventEmitter.on('data', (data) => resolve(data));
eventEmitter.on('error', (err) => reject(err));

Anywhere you bridge callback or event-based code to promises — WebSocket message handlers, test utilities, manual coordination — this is cleaner. TypeScript supports it since 5.4.

3. Iterator.prototype Methods — lazy pipelines

Arrays have had .map(), .filter(), .reduce() forever. But iterators (what you get from generators, Map.keys(), Set.values()) had nothing. The common workaround was to spread them into an array first, which materializes the whole sequence into memory before you can do anything useful.

Before — wasteful: pulls every paginated user before slicing to 10:

function* generateUsers(): Generator<User> {
  // yields users from a paginated API
}

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 is laziness. The new version only processes elements as they're needed: as soon as .take(10) has 10 emails, the pipeline stops and the generator never produces another user. No intermediate arrays, no wasted work — important when "the rest" of the iterator could be thousands more API calls.

Available methods: .map(), .filter(), .take(), .drop(), .flatMap(), .reduce(), .toArray(), .forEach(), .some(), .every(), .find(). TypeScript 5.6+.

4. RegExp.escape() — the one we've waited 15 years for

If you've ever built a regex from user input, you've either pulled in a library or written a janky escape function that probably misses an edge case.

Before:

function escapeRegex(str: string): string {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

const searchTerm = 'price: $5.00 (USD)';
const pattern = new RegExp(escapeRegex(searchTerm), 'i');

Did you remember every special character? Probably not.

After:

const searchTerm = 'price: $5.00 (USD)';
const pattern = new RegExp(RegExp.escape(searchTerm), 'i');

Small API, but it kills a whole class of bugs and a tiny security footgun. Use it any time a regex is built from external input — search, filtering, highlighting.

5. Float16Array — typed array for ML and graphics

A 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]);

Useful if you're doing ML inference in the browser, WebGPU work, or processing large numeric datasets where memory matters more than precision. For everyone else: you'll never touch it. The upside for the platform is it gives one less reason to drop into WASM for numeric work.

6. using / await using — automatic cleanup

If you've ever forgotten to close a file handle, release a DB connection, or clear a timer, this one is for you. Explicit Resource Management adds using and await using declarations that automatically call a cleanup hook when the variable goes out of scope — like Python's with or C#'s using.

Before — the dreaded try/finally pyramid:

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 pyramid is gone. Each resource cleans itself up when the block exits — whether it returns normally or throws.

The mechanism: the resource implements [Symbol.asyncDispose]() (or [Symbol.dispose]() for sync). Node's built-in file handles already do. For your own classes:

class DatabaseConnection {
  async [Symbol.asyncDispose]() {
    await this.release();
  }
}

V8 and Node.js 22+ ship it; Chrome has it; Firefox and Safari are catching up. TypeScript 5.2+. Server-side: use today. Client-side: wait a bit.

7. Promise.try() — safe function wrapping

You have a function that might be sync, might be async. You want to call it and always get a Promise back, with errors caught either way.

Before — verbose:

function runTask(task: () => unknown): Promise<unknown> {
  try {
    const result = task();
    return Promise.resolve(result);
  } catch (err) {
    return Promise.reject(err);
  }
}

A common shorter version has a subtle bug — it doesn't catch synchronous throws because task() is invoked inside the .then(), but only after the surrounding function has already returned the promise:

const result = Promise.resolve().then(() => task());

Actually that does catch them — but a naive Promise.resolve(task()) does not. Either way, getting the corner cases right by hand is error-prone.

After:

const result = Promise.try(task);

One line. Catches sync throws, wraps sync return values, passes through Promises. Useful anywhere you accept a callback that could be sync or async — plugin systems, middleware chains, test runners, task queues. It's the Promise.withResolvers of error handling.

Chrome 128+, Firefox 132+, Safari 18.2+, Node 22+. TypeScript 5.7+.

8. JSON modules and import attributes

Import attributes (the with syntax) tell the runtime how an import should be interpreted:

import config from './config.json' with { type: 'json' };
import styles from './styles.css' with { type: 'css' };

Bundlers have done this for a while; now it's spec, and runtimes have caught up — Chrome, Edge, Firefox, Safari (as of April 2025) and Node.js 22+ all support with { type: 'json' }.

The important bit: type is a security mechanism, not a hint. The runtime validates the MIME type before processing the file. If someone swaps your JSON endpoint for executable code, the import fails instead of silently running it.

One caveat: JSON modules only have a default export. The whole JSON object arrives as default — no named exports.

If you're importing JSON configs, fixtures, or static data, drop fs.readFileSync / require() and use this. TypeScript 5.3+.

Other good-to-know features

Three more shipped that most developers won't reach for, but should recognize:

  • Error.isError() — returns true if a value is an Error, even across realms. If you've ever had instanceof Error return false because the error came from a different iframe or Node vm context, this fixes it.
  • Regex pattern modifiers (?i:HELLO) — toggle flags like i, m, s for one part of a regex instead of the whole pattern. Handy when only a section needs case-insensitive matching.
  • Duplicate named capture groups/(?<id>\d+)|(?<id>[a-f]+)/v is now legal. Previously 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; no more dangling let.
  • Iterator helpers — lazy pipelines without spreading to arrays first.
  • RegExp.escape() — safely escape user input for dynamic regex.
  • using / await using — auto-cleanup for file handles, DB connections, etc. The biggest quality-of-life win in the spec.
  • Promise.try() — wraps any function (sync or async) and guarantees a Promise with errors caught.
  • Import attributes (with { type: 'json' }) — now Baseline across browsers and Node 22+.
  • Float16Array — niche; ML/graphics only.

Minimum setup to use most of this: TypeScript 5.6+, Node.js 22+. Most features are already Baseline in browsers.

#ENGINEERING#SECURITY#CI_CD#CONTENT#DEVTOOLS

Author

Petar Ivanov

The weekly builder brief

Subscribe for free. Get the signal. Skip the noise.

Get one focused email each week with 5-minute reads on product, engineering, growth, and execution - built to help you make smarter roadmap and revenue decisions.

Free forever. Takes 5 seconds. Unsubscribe anytime.

Join 1,872+ product leaders, engineers & founders already getting better every Tuesday.