DEV Community

Cover image for You're collecting async iterable results with a for-await loop. `Array.fromAsync` does it in one call.
Parsa Jiravand
Parsa Jiravand

Posted on

You're collecting async iterable results with a for-await loop. `Array.fromAsync` does it in one call.

When you have an async iterable — a ReadableStream, a generator that fetches paginated results, a database cursor — and you need its values in a plain array, you reach for a loop.

const results = [];
for await (const item of asyncSource) {
  results.push(item);
}
Enter fullscreen mode Exit fullscreen mode

That works. But it's four lines of ceremony for "give me an array of everything this produces." Array.fromAsync is the one-liner that's been missing.

The spread workaround doesn't work

If you've tried [...asyncIterable], you know it throws. Spread syntax works with synchronous iterables only. The await Promise.all([...asyncIterable]) trick fails too — the spread happens before await, which means JavaScript tries to spread a synchronous iterator that doesn't exist on the async source.

// ❌ TypeError: asyncSource is not iterable
const results = [...asyncSource];

// ❌ Also fails — spread is sync, runs before await
const results = await Promise.all([...asyncSource]);
Enter fullscreen mode Exit fullscreen mode

The for-await loop is the correct fallback. But it's exactly the kind of boilerplate a standard library should absorb.

Array.fromAsync in one call

const results = await Array.fromAsync(asyncSource);
Enter fullscreen mode Exit fullscreen mode

That's it. It pulls one value from the source, awaits it, stores it, then pulls the next — returning a fully-populated plain array when the source is exhausted.

Like Array.from(), it accepts a mapping function as the second argument:

const doubled = await Array.fromAsync(asyncNumbers, n => n * 2);
Enter fullscreen mode Exit fullscreen mode

The mapper runs after each value has been awaited. You can return a promise from the mapper too — Array.fromAsync awaits that as well before moving on.

What sources it accepts

Array.fromAsync accepts three kinds of input:

Async iterables — anything with a [Symbol.asyncIterator]() method. This is the main use case: generators, streams, cursors, any API that produces values lazily over time.

async function* paginate(cursor) {
  while (cursor.hasMore) {
    const page = await cursor.fetch();
    yield* page.items;
    cursor.advance();
  }
}

const allItems = await Array.fromAsync(paginate(cursor));
Enter fullscreen mode Exit fullscreen mode

Sync iterables with an async mapper — this replaces the common await Promise.all(array.map(async fn)) pattern, with one key difference explained below.

const users = await Array.fromAsync([1, 2, 3], async id => {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
});
Enter fullscreen mode Exit fullscreen mode

Array-like objects — objects with numeric indices and a length property, same as Array.from.

Sequential, not concurrent

This is the most important thing to understand about Array.fromAsync: it processes values one at a time, in order. It awaits each value fully before pulling the next.

This is different from Promise.all, which fires all promises concurrently and waits for all of them together.

// ✅ Use Promise.all when you want all fetches to run in parallel
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);

// ✅ Use Array.fromAsync when the source is lazy — values produced one at a time
const results = await Array.fromAsync(asyncGenerator());
Enter fullscreen mode Exit fullscreen mode

When you pass a sync array with an async mapper, Array.fromAsync fetches item 1, awaits the result, stores it, then fetches item 2. There's no parallelism inside the pipeline. If you're mapping over a known array and want concurrent fetches, Promise.all is still the right tool.

The sequential behavior is intentional for lazy sources: a generator or stream doesn't know what to produce next until you ask — you can't fan out requests that haven't been decided yet.

A practical pattern: async generator pipeline

Async generators are where Array.fromAsync earns its place. Consider a paginated API client:

async function* fetchPages(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    const data = await res.json();
    yield* data.items;
    next = data.nextPage ?? null;
  }
}

// Before: manual loop
const all = [];
for await (const item of fetchPages('/api/items')) {
  all.push(item);
}

// After: one call
const all = await Array.fromAsync(fetchPages('/api/items'));
Enter fullscreen mode Exit fullscreen mode

Same result. The generator drives pagination and Array.fromAsync collects everything, stopping when the generator returns.

Browser support

Array.fromAsync is Baseline 2024: Chrome 121 (January 2024), Firefox 119 (October 2023), Safari 17.4 (March 2024), Node.js 22. For older targets, core-js 3.38+ includes a polyfill, and the manual for-await loop is always a valid fallback.

🎮 Try it yourself

▶️ Open the interactive playground →

Runs right in your browser — poke at it and watch the concept react live.

🧠 Test yourself

Think it clicked? Take the 8-question quiz →

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.

The takeaway

Search your codebase for for await ... push patterns that end with the array being returned or used. Each one is a direct candidate for await Array.fromAsync(source). When you're mapping an async function over a sync array and want sequential execution, Array.fromAsync(array, asyncMapper) replaces the manual loop. When you want concurrent execution over a known array, stick with await Promise.all(array.map(asyncMapper)). The distinction is sequential vs parallel — know which you need before reaching for either.


Thanks for reading! Let's stay connected:

Top comments (0)