All articlesJavaScript

ES2025 Features You Should Actually Use

Learn about eight features you must adapt today in your production codebase.

Petar IvanovPetar Ivanov
10 min read
On this page

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.

1.SetMethods

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:

TSX
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:

TypeScript
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:

TypeScript
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:

TypeScript
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.prototypeMethods, 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:

TSX
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:

TypeScript
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:

TypeScript
// 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:

TypeScript
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.

TypeScript
const weights = new Float16Array([0.5, -0.3, 0.8, 0.1]);
// Half the memory of Float32Array for the same data

If 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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
// This doesn't catch synchronous throws from task()
const result = Promise.resolve().then(() => task());

After:

TypeScript
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.JSONmodules andimportattributes

Import attributes (the with syntax) let you specify how an import should be interpreted:

TypeScript
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(): returns true if the 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.js vm context, this fixes it.
  • Regex pattern modifiers ((?i:HELLO)): toggle flags like i, m, s for 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 dangling let declarations 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 using auto-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.

Related articles

Whenever you’re ready, here’s how I can help you:

  1. 1.

    The Conscious React: React architecture, design & clean code — 100+ production tips across 6 chapters, updated for React 19, plus 4 companion repos you can clone and run.

  2. 2.

    The Conscious Node: Node.js architecture, design & clean code — 157 production tips across 10 chapters, from module boundaries to the transactional outbox and zero-downtime deploys.

  3. 3.

    The JavaScript Architect Bundle: Both books + all React companion repos + CLAUDE.md rulesets + both playbooks. The complete path from developer to architect.

  4. 4.

    Free Resources: Architecture playbooks, cheat-sheets, and the JavaScript Architect Roadmap — practical guides for leveling up to senior.

The T-Shaped Dev

Join 30K+ engineers leveling up to architect

One practical tip on JavaScript, React, Node.js, and software architecture every week. No spam, unsubscribe anytime.

Petar Ivanov

Written by

Petar Ivanov

Software engineer, author, and speaker. I help JavaScript developers grow from Mid → Senior → Architect — production-grade React, Node.js, and AI systems.