Array.fromAsync

You have an async generator that yields pages of results, one network round-trip at a time. You want all of them in a single array. The honest way to write that today is a loop:

const users = [];
for await (const user of fetchAllUsers()) {
  users.push(user);
}

It works. It’s also four lines of boilerplate that do nothing interesting — declare an accumulator, loop, push, done. Array.fromAsync collapses it into one expression:

const users = await Array.fromAsync(fetchAllUsers());

That’s the whole pitch, but there’s real machinery underneath, and one performance trap that has bitten a lot of people. Let’s take it apart.

What it is

Array.fromAsync is the asynchronous twin of Array.from. Where Array.from turns any iterable or array-like into an array synchronously, Array.fromAsync handles sources whose values arrive over time — it consumes an async iterable, awaits each value, and hands you back a Promise that resolves to the finished array.

Array.fromAsync(items)
Array.fromAsync(items, mapFn)
Array.fromAsync(items, mapFn, thisArg)
  • items — an async iterable, a sync iterable, or an array-like object.
  • mapFn (optional) — called on every element as it’s collected; its return value (awaited) goes into the array instead.
  • thisArg (optional) — the this binding for mapFn.

The return value is always a Promise. That single fact catches beginners constantly, so say it out loud: Array.fromAsync returns a promise, not an array. You almost always await it.

Array.fromsync iterableArray (now)returns immediatelyArray.fromAsyncasync iterablePromiseArray (later)await resolves it once every value has settled
Array.from is synchronous and returns an array immediately. Array.fromAsync pulls values over time and hands back a promise that later resolves to the array.

The simplest case: draining an async generator

Async generators produce values one at a time, and each value may involve waiting — a timer, a fetch, a disk read. Here’s one that yields five numbers with a growing delay between them:

async function* countUp() {
  for (let i = 0; i < 5; i++) {
    await new Promise(r => setTimeout(r, 10 * i));
    yield i;
  }
}

const nums = await Array.fromAsync(countUp());
console.log(nums); // [0, 1, 2, 3, 4]

Array.fromAsync walks the generator to completion, waiting for each yield in turn, and collects everything it produced. No accumulator, no loop, no push.

fromAsync draining an async generator
1/7
Variables
result="[]"
fromAsync starts with an empty internal array and asks the generator for its first value.

Notice the rhythm in that walkthrough: pull one, wait for it, then pull the next. Nothing is requested from the generator until the previous value has settled. That laziness is the defining trait of Array.fromAsync, and it’s exactly what you want when the source is a stream — you don’t ask for page 2 until page 1 is in hand.

The trap: this is sequential, not parallel

Here’s where people get hurt. Array.fromAsync also accepts a plain sync iterable whose elements happen to be promises. It will await each one — one after another.

function* makePromises() {
  for (let i = 0; i < 5; i++) {
    yield new Promise(r => setTimeout(() => r(i), 100));
  }
}

console.time("fromAsync");
await Array.fromAsync(makePromises());
console.timeEnd("fromAsync"); // ~503ms

console.time("Promise.all");
await Promise.all(makePromises());
console.timeEnd("Promise.all"); // ~101ms

Five 100-millisecond promises. Promise.all finishes in ~100ms because it kicks all five off and waits for the slowest. Array.fromAsync takes ~500ms because it awaits the first fully, then awaits the second, and so on. The timers were already running in this toy example, but the pattern generalizes: if each element is a fresh fetch that only starts when it’s awaited, fromAsync serializes your network calls.

Promise.all — parallelreq 1req 2req 3req 4done ≈ 1×Array.fromAsync — sequentialreq 1req 2req 3req 4done ≈ 4×
Promise.all starts every request at once and finishes with the slowest. Array.fromAsync awaits each element before starting the next, so total time is the sum.

Don’t take the figure’s word for it — run the same five 100 ms promises both ways and read the clock. Array.fromAsync awaits them in series; Promise.all fires them at once.

interactiveSequential fromAsync vs parallel Promise.all

So when is the sequential behavior the feature rather than the bug? When the work genuinely depends on order or shared state: draining a stream, paginating an API where each page’s cursor comes from the last response, or throttling calls to a rate-limited service on purpose.

The real payoff: paginated APIs

This is the use case Array.fromAsync was made for. Say an endpoint returns results one page at a time, each response carrying a cursor for the next page. Model it as an async generator — each yield is one item, and the generator itself handles the paging:

async function* fetchAllRepos(org) {
  let url = `https://api.example.com/orgs/${org}/repos?per_page=100`;
  while (url) {
    const res = await fetch(url);
    const { items, nextPage } = await res.json();
    yield* items;              // hand each repo up one at a time
    url = nextPage;            // null on the last page → loop ends
  }
}

Now collecting every repository across every page is a single line:

const repos = await Array.fromAsync(fetchAllRepos("acme"));
console.log(repos.length); // however many there are, across N pages

The sequential behavior is correct here — you cannot request page 2 until page 1 tells you its nextPage. Each network round-trip depends on the previous one. Promise.all couldn’t do this job; there’s no array of promises to hand it, because the promises don’t exist until the cursors are known.

Page 1a, b, cPage 2d, ePage 3fasync generatoryield* itemsawait Array.fromAsync(…)abcdefone flat array, all pages
An async generator fetches pages in order, yielding each item. fromAsync accumulates every yielded value across all pages into one flat array.

The mapFn, and the awaiting rules

Like Array.from, the second argument is a transform applied to each element as it’s collected. What’s special here: the return value of mapFn is awaited. So you can map to a promise and get its resolved value in the array.

const ids = [1, 2, 3];
const users = await Array.fromAsync(ids, id => fetch(`/u/${id}`).then(r => r.json()));
// users is the resolved JSON for each id — but fetched one at a time

That reads nicely, but remember the sequential rule: those fetches run in series. If the three ids are independent, await Promise.all(ids.map(id => ...)) is faster. Reach for the mapFn when the transform and the collection belong together and order matters.

The mapping callback receives (element, index) — just two arguments. There is no third “array” parameter the way Array.from’s callback has one, because the target array doesn’t exist yet while you’re still building it.

Now the subtle part. Whether the input to mapFn is awaited depends on where the source came from:

That asymmetry produces a genuinely surprising result. An async iterable that yields promises, with no mapFn, gives you an array of promises — they’re never awaited:

async function* yieldsPromises() {
  yield Promise.resolve(1);
  yield Promise.resolve(2);
}

await Array.fromAsync(yieldsPromises());
// [Promise, Promise]  — NOT [1, 2]

But add even a trivial mapFn and the picture changes, because the mapped output is awaited:

await Array.fromAsync(yieldsPromises(), x => x);
// [1, 2]  — the identity map forces the await

See the asymmetry live. This demo drains an async source whose elements are themselves promises. With no mapFn, those promises pass straight through unawaited; add an identity mapFn and each result gets awaited on the way into the array.

interactivemapFn forces the await

This is rarely what you build on purpose, but it explains confusing output when it happens. If you control the generator, just yield 1 instead of yield Promise.resolve(1) and the question disappears.

Sync iterable / array-likeelementawaitmapFnawaitarrayAsync iterableelementpassed raw (no await)mapFnawaitarray
Awaiting depends on the source. Sync sources await elements before mapFn; async sources pass elements through raw. The mapFn result is always awaited.

Errors and cleanup

If the async iterator throws or yields a rejected promise, the promise from Array.fromAsync rejects with that reason. Wrap the await in try/catch as you would any async call:

try {
  const all = await Array.fromAsync(mightFail());
} catch (err) {
  // the first error to occur stops collection and lands here
}

There’s a sharp edge worth knowing for the sync-iterable-of-promises case. If one of those promises rejects, Array.fromAsync does not call the sync iterator’s return() method, so a generator’s finally block won’t run — the iterator isn’t closed cleanly.

function* gen() {
  try {
    yield 0;
    yield Promise.reject(new Error("boom"));
  } finally {
    console.log("cleanup"); // does NOT run under Array.fromAsync
  }
}

try {
  await Array.fromAsync(gen());
} catch (e) {
  console.log("caught", e.message); // caught boom
}

If that generator holds a resource — an open file, a database cursor — its cleanup silently gets skipped. When you need guaranteed cleanup over a sync iterable of promises, drop back to an explicit loop, which does close the iterator:

const arr = [];
try {
  for (const val of gen()) {
    arr.push(await val); // for...of closes the iterator on throw → finally runs
  }
} catch (e) {
  console.log("caught", e.message);
}

Availability and support

Array.fromAsync reached Stage 4 and is part of ES2026. It’s been Baseline (newly available) since January 2024, meaning the current versions of Chrome, Edge, Firefox, and Safari all ship it, and Node and Deno have it too. It’s on track to reach Baseline widely available status in mid-2026.

“Newly available” is the caveat to respect: if you must support browsers or runtimes from before 2024, feature-detect or polyfill. A one-line guard is enough to branch:

if (typeof Array.fromAsync === "function") {
  // native path
} else {
  // fall back to a for-await loop, or pull in a polyfill (e.g. core-js)
}

Because the semantics are precisely specified, the core-js polyfill matches native behavior closely, including the sequential awaiting. Don’t assume a hand-rolled Array.from(await Promise.all(...)) shim is equivalent — that one runs in parallel and changes timing.

Summary

  • Array.fromAsync(items, mapFn?, thisArg?) builds an array from an async iterable, sync iterable, or array-like, and returns a promise — you almost always await it.
  • It awaits values sequentially and lazily: it doesn’t request the next value until the current one has settled. That’s the point when consuming streams or cursor-paginated APIs.
  • It is not a parallel tool. For independent concurrent work, Promise.all is far faster; swapping it for fromAsync can serialize N calls into N× the latency.
  • The optional mapFn gets (element, index) and its return value is awaited. For a sync source, elements are awaited before mapFn; for an async source, they’re passed raw, so an async iterable of promises with no mapFn yields an array of unresolved promises.
  • Errors reject the returned promise; catch them with try/catch. Over a sync iterable of promises, a rejection skips the iterator’s finally — use an explicit for...of loop when cleanup must run.
  • Stage 4 / ES2026, Baseline since January 2024. Feature-detect or polyfill for pre-2024 targets; it can’t be transpiled.