Array.fromAsync, Promise.try and RegExp.escape: Three ES2026 Features Worth Using Today
A practical guide to the most useful ES2026 additions — Array.fromAsync for collecting async iterables, Promise.try for unifying sync/async calls, and RegExp.escape for safe dynamic regexes.
Most JavaScript language updates ship one headline feature and a pile of trivia. ES2026 is the opposite: no single blockbuster, but three small utilities — Array.fromAsync, Promise.try and RegExp.escape — that each delete a helper function you have probably written by hand at some point. This post is a practical tour of all three: what they replace, where they shine, and the edge cases worth knowing before you rely on them.
Array.fromAsync: collect an async iterable without the loop
Array.from has been the workhorse for turning array-likes and iterables into real arrays since ES2015 — but it is strictly synchronous. The moment your data source is an async iterator (paginated APIs, streams, async generators), you fall back to the same ceremony every time: create an empty array, for await over the source, push each item.
// The pattern we have all written a hundred times
async function collect(source) {
const items = [];
for await (const item of source) {
items.push(item);
}
return items;
}Array.fromAsync is exactly that function, built into the language:
async function* fetchPages() {
let url = "/api/items?page=1";
while (url) {
const res = await fetch(url);
const data = await res.json();
yield* data.items;
url = data.nextPage;
}
}
const allItems = await Array.fromAsync(fetchPages());It mirrors Array.from closely: it accepts async iterables, plain iterables, and array-likes, and it takes an optional mapping function as the second argument. Two details matter in practice. First, the mapping function may itself be async — each returned promise is awaited before the value lands in the array. Second, items are awaited sequentially, one at a time. That makes Array.fromAsync the right tool for ordered consumption of a stream, and the wrong tool for firing off requests in parallel — for parallelism you still want Promise.all:
// Sequential: each fetch waits for the previous one
const users = await Array.fromAsync(ids, (id) => fetchUser(id));
// Parallel: all fetches start immediately
const usersFast = await Promise.all(ids.map((id) => fetchUser(id)));Promise.try: one entry point for sync and async functions
Suppose you accept a callback that might be synchronous, might be asynchronous, and might throw synchronously. Wrapping it safely has always been awkward. Promise.resolve(fn()) looks right but is subtly wrong: if fn throws synchronously, the exception escapes before Promise.resolve ever runs, so your .catch never sees it.
// Buggy: a synchronous throw in fn() is NOT caught
Promise.resolve(fn()).catch(handleError);
// The old workaround: an immediately-run async wrapper
(async () => fn())().catch(handleError);Promise.try(fn) closes the gap. It calls fn immediately and synchronously; if fn returns a value you get a fulfilled promise, if it returns a promise you get that promise's outcome, and if it throws you get a rejected promise. Every failure mode flows into one channel:
function runStep(step) {
return Promise.try(step)
.then((result) => log("ok", result))
.catch((err) => log("failed", err));
}
runStep(() => JSON.parse(rawInput)); // sync, may throw
runStep(() => fetch("/api/health")); // async
runStep(() => 42); // plain valueThe immediate-execution detail is the point: unlike wrapping in setTimeout or an async IIFE that defers to the microtask queue, Promise.try runs the synchronous part of fn right away, preserving ordering guarantees while still normalising the result. It also passes extra arguments through — Promise.try(fn, a, b) calls fn(a, b) — which avoids allocating a closure in hot paths. If you maintain plugin systems, middleware runners, or anything that executes user-supplied callbacks, this is the cleanest contract available.
RegExp.escape: the utility everyone hand-rolled, finally standard
Building a regular expression from user input has been a known footgun forever: any character like ., +, ( or ? in the input changes the pattern's meaning, and in the worst case opens the door to pathological backtracking. Every codebase grew its own escapeRegExp helper — famously recommended by MDN itself — with subtly different character sets.
// Before: the hand-rolled helper in every utils file
function escapeRegExp(str) {
return str.replace(/[.*+?^$()|[\]\\{}]/g, "\\$&");
}
// After: built in
const query = "price (USD)?";
const re = new RegExp(RegExp.escape(query), "i");
"What is the price (USD)?".search(re); // matches literallyRegExp.escape returns a string in which every syntax character is escaped so the input matches literally. The standard version is more thorough than most homegrown helpers — it also escapes characters that only matter in edge positions, so the result is safe to concatenate into any part of a pattern. Typical uses: highlight-search-term features, converting user glob input, building dynamic word filters. One niche caveat: it escapes aggressively enough that the output is meant for pattern construction, not for display.
One habit worth keeping even with RegExp.escape: if you are matching a plain substring with no flags or boundaries, String.prototype.includes is still simpler and faster. Reach for the regex only when you need case-insensitivity, boundaries, or alternation.
Support and adoption strategy
Where these stand as of August 2026:
Array.fromAsynchas the widest support of the three — all evergreen browsers and Node.js 22+.Promise.tryandRegExp.escapeare newer: current evergreen browsers and recent Node releases (Node 24 line) ship both. Check your minimum supported runtime before dropping the fallback.- All three are trivially polyfillable — core-js covers them, and each can also be inlined as a five-line fallback if you avoid polyfill dependencies.
Adoption advice: these are drop-in replacements for helpers you likely already have, so the migration is mechanical — swap the implementation inside your existing collect, tryFn and escapeRegExp utilities first, keep the call sites, and delete the wrappers once your runtime floor allows it. If you enjoyed this kind of incremental-language-win tour, the same philosophy applies to my earlier posts on iterator helpers and the new Set methods — ES2026 continues exactly that trend: less boilerplate, fewer utils files, no new mental model required.