Array.fromAsync in Practice: Turning Async Iterables into Arrays Cleanly
AI generated
JS
() =>
JavaScript · ES2024 · Async Iterables · Array Methods
Array.fromAsync in Practice
Turning Async Iterables into Arrays Cleanly

Anyone who has turned an asynchronous data stream into an array with manual loops and push calls knows the race conditions and the awkward error handling that come with it. Array.fromAsync solves exactly this problem: a single built in method that collects async iterables, generators and mixed promise lists in the correct order into an array.

14 min read Array.fromAsync · Async Generators · Pagination · Error Handling Node 22+ · Chrome 121+ · ES2024

1. The Problem: Collecting Async Iterables Manually into Arrays

Before Array.fromAsync existed, every asynchronous data stream had to be manually turned into an array. The typical approach: create an empty array, run a for await...of loop over the async iterable and append an element with push on every iteration. That works, but it is boilerplate rewritten in every project, and it obscures the actual intent of the code: producing a complete array from an asynchronous source.

The second problem was error handling. If the iteration aborts halfway through because a network request fails, the partially filled array is often left dangling somewhere, without the caller cleanly recognizing where exactly the abort happened. Array.fromAsync encapsulates this logic in a single built in method, throws a regular exception on failure and makes the code at the call site far more readable than a manually written collection loop.

2. Array.fromAsync: Basic Syntax and Difference from Array.from

Array.fromAsync is the asynchronous counterpart to Array.from. While Array.from consumes synchronous iterables and array like objects, Array.fromAsync additionally accepts async iterables, meaning anything that implements Symbol.asyncIterator. The key difference: Array.fromAsync always returns a promise that only resolves once every element has been collected.

The signature matches that of Array.from: the first argument is the source, an optional mapping function as the second argument, and an optional thisArg as the third. The decisive advantage over a hand written loop lies in the consistency: Array.fromAsync works identically for async generators, streams and plain arrays of promise values, without having to write custom collection logic for every case.


// Array.fromAsync consumes any async iterable
async function* fetchPages() {
  yield "page-1";
  yield "page-2";
  yield "page-3";
}

const pages = await Array.fromAsync(fetchPages());
console.log(pages); // ["page-1", "page-2", "page-3"]

// Compare: the manual pattern before Array.fromAsync existed
async function collectManually(source) {
  const result = [];
  for await (const item of source) {
    result.push(item);
  }
  return result;
}

3. Consuming Async Generators with Array.fromAsync

Async generators are the most common source for Array.fromAsync. An async generator yields its values one after another with yield, and arbitrarily long asynchronous operations such as network requests or database queries can happen between the values. Array.fromAsync waits at every yield until the next value is ready and collects all values at the end in the order they were produced.

This is especially useful when a data source works lazily internally, meaning it only computes values on demand. A generator reading lines from a file or yielding entries from a database cursor iteration does not have to change its internal logic to cooperate with Array.fromAsync. As long as the object correctly implements Symbol.asyncIterator, the collection works without adaptation.

4. Mapping Function: Transforming While Collecting

Just like Array.from, Array.fromAsync also accepts an optional mapping function as the second argument. This function itself can be synchronous or asynchronous. If the mapping function is asynchronous, Array.fromAsync waits for it to resolve before requesting the next value from the source. This allows raw data to be transformed directly while it is being collected, instead of running a separate map pass afterward.

A typical use case: a list of IDs is collected asynchronously, and the mapping function loads the associated detail data for each ID. Without Array.fromAsync one would first have to collect all IDs and then load the details in a second Promise.all round. With the built in mapping function, both happen in a single, clearly readable expression.


async function* userIds() {
  yield 1;
  yield 2;
  yield 3;
}

async function fetchUserDetail(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

// Mapping function runs for each yielded value, awaited in order
const users = await Array.fromAsync(userIds(), async (id) => fetchUserDetail(id));
console.log(users.length); // 3

5. Practical Example: Collecting Paginated API Results

Paginated REST APIs are a classic use case for Array.fromAsync. Instead of writing a while loop with manual cursor handling, the pagination logic is encapsulated in an async generator that loads page after page and passes individual items along via yield*. Array.fromAsync then only has to handle the collecting, while the generator keeps the actual network logic isolated.

This pattern cleanly separates two responsibilities: the generator knows how to navigate from page to page, and the caller only decides whether it needs all results as an array or would rather iterate itself with for await. The same generator function can therefore be reused both for Array.fromAsync and for memory friendly streaming processing, without duplicating code.


async function* paginatedResults(baseUrl) {
  let url = baseUrl;
  while (url) {
    const res = await fetch(url);
    const data = await res.json();
    yield* data.items;       // yield each item individually
    url = data.nextPageUrl;  // null ends the generator
  }
}

// Collect every item across all pages into one array
const allItems = await Array.fromAsync(paginatedResults("/api/products?page=1"));
console.log(`Loaded ${allItems.length} products across all pages`);

6. Error Handling with Array.fromAsync

If the source throws an error during iteration, whether in the generator itself or in an awaited network operation, the promise returned by Array.fromAsync rejects. The caller catches this error with a regular try/catch around the await expression, exactly like with any other promise. Elements already collected are lost in the process, because Array.fromAsync does not return a partial result, only a complete array or a rejection.

Anyone who wants to keep partial results even on failure has to implement the collection themselves with for await and handle errors per element, instead of using Array.fromAsync. For most use cases, such as loading a finished result list, the all or nothing behavior is exactly right: an incomplete array would rarely be sensible to process further anyway.


async function* riskySource() {
  yield 1;
  yield 2;
  throw new Error("Network timeout on item 3");
}

try {
  const result = await Array.fromAsync(riskySource());
  console.log(result);
} catch (err) {
  // The rejection carries the original error, no partial array is returned
  console.error("Collection failed:", err.message);
}

7. Mixed Lists of Values and Promises

Besides real async iterables, Array.fromAsync also accepts ordinary synchronous iterables whose elements are themselves promises. This is a difference to Promise.all(array): Array.fromAsync resolves the promises one after another, while Promise.all starts all of them at once. If the source contains a mix of plain values and promises, promises are awaited and plain values are passed through unchanged.

Understanding this behavior is important before using Array.fromAsync as a replacement for Promise.all: for many independent promises meant to run in parallel, Promise.all remains the right choice because it kicks off every operation at the same time. Array.fromAsync is instead the right choice when the order of processing matters or when the source itself is a genuine async iterable with its own timing.

8. Sequential Processing, Order and Performance

A common misunderstanding: Array.fromAsync does not parallelize anything on its own. Every value is requested, awaited and only then is the next value requested from the source. For an async generator making its own network request per yield, this means the requests run sequentially, not in parallel. Anyone who needs real parallelism has to parallelize the requests themselves with Promise.all and can optionally process the resulting array with Array.fromAsync afterward.

For many practical cases, the sequential nature is exactly what is desired: with paginated APIs, for instance, the next page can only be requested once the current page delivers the cursor for it. Here the sequential behavior of Array.fromAsync fits the nature of the problem exactly, without forcing artificial parallelism that would not even be possible for this use case anyway.

9. Array.fromAsync Compared to Alternatives

The choice between Array.fromAsync, a manual for await loop and Promise.all depends on the concrete data source and the desired concurrency behavior. The following table summarizes when which pattern is the better choice.

Task Without Array.fromAsync With Array.fromAsync Advantage
Collecting an async generator for await + push Array.fromAsync(gen()) Less boilerplate, clearer intent
Transforming while collecting Collect, then separate map Mapping function as 2nd argument One pass instead of two
Parallel promises Promise.all(array) Array.fromAsync (sequential) Promise.all remains advantageous here
Paginated API Manual while cursor loop Generator + Array.fromAsync Logic separated, reusable
Error mid iteration Must be handled manually Automatic promise rejection Consistent error behavior

In practice these approaches complement rather than replace each other: Array.fromAsync handles the collecting from a sequential source, Promise.all remains responsible for genuine parallelism, and a manual loop is only needed where partial results must survive even a failure.

Mironsoft

Modern JavaScript development and frontend architecture

Async data flows that stay reliable and readable?

We modernize existing JavaScript code, replace fragile manual collection loops with Array.fromAsync and other ES2024 patterns, and ensure clean error behavior across your frontend and Node applications.

Code Modernization

Replacing legacy loops with modern array and async patterns

API Integration

Cleanly connecting paginated interfaces with async generators

Code Review

Checking error handling and concurrency in existing code

10. Summary

Array.fromAsync replaces one of the most common manually written loops in asynchronous JavaScript: collecting values from an async iterable into a plain array. Instead of combining for await...of with manual push, a single built in method handles waiting for each value, the optional transformation via a mapping function, and error handling via a regular promise rejection.

It is important to understand that Array.fromAsync works sequentially and does not offer automatic parallelism. For genuine parallel processing, Promise.all remains the right choice, while Array.fromAsync shines wherever order matters or the source itself is an async generator with its own timing, such as paginated APIs, file streams or database cursors.

Array.fromAsync — Key Points at a Glance

Basic Function

Collects async iterables, async generators and promise lists into a plain array. Returns a promise itself.

Mapping Function

Second argument transforms each value directly while collecting, synchronously or asynchronously, saving a second pass.

Order & Performance

Works sequentially. For parallel promises, Promise.all remains the more suitable choice.

Error Handling

An error during iteration leads to a promise rejection, no partial array is returned.

11. FAQ: Array.fromAsync in Practice

1What exactly does Array.fromAsync do?
Collects values from async iterables, async generators or promise lists into a plain array and returns a promise itself.
2Difference from Array.from?
Array.from is purely synchronous. Array.fromAsync additionally accepts Symbol.asyncIterator objects and waits for each value.
3Does Array.fromAsync parallelize?
No, processing is sequential. For genuine parallelism, Promise.all remains the right choice.
4Can I transform while collecting?
Yes, via the second argument, an optional synchronous or asynchronous mapping function.
5What happens on an error?
The returned promise rejects, catchable with try/catch. There is no partial array on an error.
6Does it work with paginated APIs?
Very well, an async generator with yield* per page can be passed directly to Array.fromAsync.
7Which environments support it?
Node from 22, Chrome from 121, current Firefox and Safari. Older runtimes need a polyfill.
8Replacement for Promise.all?
No, Promise.all remains the better choice for independent parallel operations.
9Also for synchronous iterables with promises?
Yes, promises are awaited, plain values are passed through unchanged.
10Good first use case?
An async generator for a paginated REST API elegantly replaces a manual while cursor loop.