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) — thethisbinding formapFn.
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.
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.
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.
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.
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.
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.
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.
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 alwaysawaitit.- 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.allis far faster; swapping it forfromAsynccan serialize N calls into N× the latency. - The optional
mapFngets(element, index)and its return value is awaited. For a sync source, elements are awaited beforemapFn; for an async source, they’re passed raw, so an async iterable of promises with nomapFnyields 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’sfinally— use an explicitfor...ofloop when cleanup must run. - Stage 4 / ES2026, Baseline since January 2024. Feature-detect or polyfill for pre-2024 targets; it can’t be transpiled.