• Stars
    star
    178
  • Rank 214,989 (Top 5 %)
  • Language
    HTML
  • License
    BSD 3-Clause "New...
  • Created over 3 years ago
  • Updated 11 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Draft specification for a proposed Array.fromAsync method in JavaScript.

Array.fromAsync for JavaScript

ECMAScript Stage-2 Proposal. J. S. Choi, 2021.

Why an Array.fromAsync method

Since its standardization in JavaScript, Array.from has become one of Array’s most frequently used built-in methods. However, no similar functionality exists for async iterators.

const arr = [];
for (const v of iterable) {
  arr.push(v);
}

// This does the same thing.
const arr = Array.from(iterable);

Such functionality would also be useful for dumping the entirety of an async iterator into a single data structure, especially in unit tests or in command-line interfaces. (Several real-world examples are included in a following section.)

const arr = [];
for await (const v of asyncIterable) {
  arr.push(v);
}

// We should add something that does the same thing.
const arr = await ??????????(asyncIterable);

There is an it-all NPM library that performs only this task and which gets about 50,000 weekly downloads. This of course does not include any code that uses ad-hoc for awaitof loops with empty arrays. Further demonstrating the demand for such functionality, several Stack Overflow questions have been asked by various developers, asking how to convert async iterators to arrays.

There are several real-world examples listed later in this explainer.

Description

(A formal draft specification is available.)

Array.fromAsync is to for await
as Array.from is to for.

Similarly to Array.from, Array.fromAsync would be a static method of the Array built-in class, with one required argument and two optional arguments: (items, mapfn, thisArg).

Async-iterable inputs

But, instead of converting a sync iterable to an array, Array.fromAsync can convert an async iterable to a promise that (if everything goes well) will resolve to a new array. Before the promise resolves, it will create an async iterator from the input, lazily iterate over it, and add each yielded value to the new array. (The promise is immediately returned after the Array.fromAsync function call, no matter what.)

async function * asyncGen (n) {
  for (let i = 0; i < n; i++)
    yield i * 2;
}

// `arr` will be `[0, 2, 4, 6]`.
const arr = [];
for await (const v of asyncGen(4)) {
  arr.push(v);
}

// This is equivalent.
const arr = await Array.fromAsync(asyncGen(4));

Sync-iterable inputs

If the argument is a sync iterable (and not an async iterable), then the return value is still a promise that will resolve to an array. If the sync iterator yields promises, then each yielded promise is awaited before its value is added to the new array. (Values that are not promises are also awaited to prevent Zalgo.) All of this matches the behavior of for await.

function * genPromises (n) {
  for (let i = 0; i < n; i++)
    yield Promise.resolve(i * 2);
}

// `arr` will be `[ 0, 2, 4, 6 ]`.
const arr = [];
for await (const v of genPromises(4)) {
  arr.push(v);
}

// This is equivalent.
const arr = await Array.fromAsync(genPromises(4));

Like for await, Array.fromAsync lazily iterates over a sync-but-not-async input. Whenever a developer needs to dump a synchronous input that yields promises into an array, the developer needs to choose carefully between Array.fromAsync and Promise.all, which have complementary control flows:

Parallel awaiting Sequential awaiting
Lazy iteration Impossible await Array.fromAsync(input)
Eager iteration await Promise.all(Array.from(input)) Useless

Also like for await, when given a sync-but-not-async iterable input, then Array.fromAsync will catch only the first rejection that its iteration reaches, and only if that rejection does not occur in a microtask before the iteration reaches and awaits for it. For more information, see § Errors.

// `arr` will be `[ 0, 2, 4, 6 ]`.
// `genPromises(4)` is lazily iterated,
// and its four yielded promises are awaited in sequence.
const arr = await Array.fromAsync(genPromises(4));

// `arr` will also be `[ 0, 2, 4, 6 ]`.
// However, `genPromises(4)` is eagerly iterated
// (into an array of four promises),
// and the four promises are awaited in parallel.
const arr = await Promise.all(Array.from(genPromises(4)));

Non-iterable array-like inputs

Array.fromAsync’s valid inputs are a superset of Array.from’s valid inputs. This includes non-iterable array-likes: objects that have a length property as well as indexed elements (similarly to Array.prototype.values). The return value is still a promise that will resolve to an array. If the array-like object’s elements are promises, then each accessed promise is awaited before its value is added to the new array.

One TC39 representative’s opinion: “[Array-likes are] very much not obsolete, and it’s very nice that things aren’t forced to implement the iterator protocol to be transformable into an Array.”

const arrLike = {
  length: 4,
  0: Promise.resolve(0),
  1: Promise.resolve(2),
  2: Promise.resolve(4),
  3: Promise.resolve(6),
}

// `arr` will be `[ 0, 2, 4, 6 ]`.
const arr = [];
for await (const v of Array.from(arrLike)) {
  arr.push(v);
}

// This is equivalent.
const arr = await Array.fromAsync(arrLike);

As it does with sync-but-not-async iterable inputs, Array.fromAsync lazily iterates over the values of array-like inputs, and it awaits each value. The developer must choose between using Array.fromAsync and Promise.all (see § Sync-iterable inputs and § Errors).

Generic factory method

Array.fromAsync is a generic factory method. It does not require that its this receiver be the Array constructor. fromAsync can be transferred to or inherited by any other constructor. In that case, the final result will be the data structure created by that constructor (with no arguments), and with each value yielded by the input being assigned to the data structure’s numeric properties. (Symbol.species is not involved at all.) If the this receiver is not a constructor, then fromAsync creates an array as usual. This matches the behavior of Array.from.

async function * asyncGen (n) {
  for (let i = 0; i < n; i++)
    yield i * 2;
}
function Data (n) {}
Data.from = Array.from;
Data.fromAsync = Array.fromAsync;

// d will be a `new Data(0)`, with its `0` property assigned to `0`, its `1`
// property assigned to `2`, etc.
const d = new Data(0); let i = 0;
for await (const v of asyncGen(4)) {
  d[i] = v;
}

// This is equivalent.
const d = await Data.fromAsync(asyncGen(4));

Optional parameters

Array.fromAsync has two optional parameters: mapfn and thisArg.

mapfn is an optional mapping callback, which is called on each value yielded from the input (and awaited if it came from a synchronous input), along with its index integer (starting from 0). Each result of the mapping callback is, in turn, awaited then added to the array.

(Without the optional mapping callback, each value yielded from asynchronous inputs is not awaited, and each value yielded from synchronous inputs is awaited only once, before the value is added to the result array. This matches the behavior of for await.)

thisArg is a this-binding receiver value for the mapping callback. By default, this is undefined. These optional parameters match the behavior of Array.from. Their exclusion would be surprising to developers who are already used to Array.from.

async function * asyncGen (n) {
  for (let i = 0; i < n; i++)
    yield i * 2;
}

// `arr` will be `[ 0, 4, 16, 36 ]`.
const arr = [];
for await (const v of asyncGen(4)) {
  arr.push(await (v ** 2));
}

// This is equivalent.
const arr = await Array.fromAsync(asyncGen(4), v =>
  v ** 2);

Errors

Like other promise-based APIs, Array.fromAsync will always immediately return a promise. Array.fromAsync will never synchronously throw an error and summon Zalgo.

When Array.fromAsync’s input throws an error while creating its async or sync iterator, then Array.fromAsync’s returned promise will reject with that error.

const err = new Error;
const badIterable = { [Symbol.iterator] () { throw err; } };

// This returns a promise that will reject with `err`.
Array.fromAsync(badIterable);

When Array.fromAsync’s input is iterable but the input’s iterator throws while iterating, then Array.fromAsync’s returned promise will reject with that error.

const err = new Error;
async function * genErrorAsync () { throw err; }

// This returns a promise that will reject with `err`.
Array.fromAsync(genErrorAsync());
const err = new Error;
function * genError () { throw err; }

// This returns a promise that will reject with `err`.
Array.fromAsync(genError());

When Array.fromAsync’s input is synchronous only (i.e., the input is not an async iterable), and when one of the input’s values is a promise that eventually rejects or has rejected, then iteration stops and Array.fromAsync’s returned promise will reject with the first such error.

In this case, Array.fromAsync will catch and handle that first input rejection only if that rejection does not occur in a microtask before the iteration reaches and awaits for it.

const err = new Error;
function * genRejection () {
  yield Promise.reject(err);
}

// This returns a promise that will reject with `err`. There is **no**
// unhandled promise rejection, because the rejection occurs in the same
// microtask.
Array.fromAsync(genZeroThenRejection());

Just like with for await, Array.fromAsync will not catch any rejections by the input’s promises whenever those rejections occur before the ticks in which Array.fromAsync’s iteration reaches those promises.

This is because – like for await – Array.fromAsync lazily iterates over its input and sequentially awaits each yielded value. Whenever a developer needs to dump a synchronous input that yields promises into an array, the developer needs to choose carefully between Array.fromAsync and Promise.all, which have complementary control flows (see § Sync-iterable inputs).

For example, when a synchronous input contains two promises, the latter of which will reject before the former promise resolves, then Array.fromAsync will not catch that rejection, because it lazily reaches the rejecting promise only after it already has rejected.

const numOfMillisecondsPerSecond = 1000;
const slowError = new Error;
const fastError = new Error;

function waitThenReject (value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(value), numOfMillisecondsPerSecond);
  });
}

function * genRejections () {
  // Slow promise.
  yield waitAndReject(slowError);
  // Fast promise.
  yield Promise.reject(fastError);
}

// This returns a promise that will reject with `slowError`. There is **no**
// unhandled promise rejection: the iteration is lazy and will stop early at the
// slow promise, so the fast promise will never be created.
Array.fromAsync(genSlowRejectThenFastReject());

// This returns a promise that will reject with `slowError`. There **is** an
// unhandled promise rejection with `fastError`: the iteration eagerly creates
// and dumps both promises into an array, but Array.fromAsync will
// **sequentially** handle only the slow promise.
Array.fromAsync([ ...genSlowRejectThenFastReject() ]);

// This returns a promise that will reject with `fastError`. There is **no**
// unhandled promise rejection: the iteration eagerly creates and dumps both
// promises into an array, but Promise.all will handle both promises **in
// parallel**.
Promise.all([ ...genSlowRejectThenFastReject() ]);

When Array.fromAsync’s input has at least one value, and when Array.fromAsync’s mapping callback throws an error when given any of those values, then Array.fromAsync’s returned promise will reject with the first such error.

const err = new Error;
function badCallback () { throw err; }

// This returns a promise that will reject with `err`.
Array.fromAsync([ 0 ], badCallback);

When Array.fromAsync’s input is null or undefined, or when Array.fromAsync’s mapping callback is neither undefined nor callable, then Array.fromAsync’s returned promise will reject with a TypeError.

// These return promises that will reject with TypeErrors.
Array.fromAsync(null);
Array.fromAsync([], 1);

Other proposals

Relationship with iterator-helpers

The iterator-helpers proposal has toArray, which works with both sync and async iterables. The following pairs of lines are equivalent:

// Array.from

Array.from(iterable)
Iterator(iterable).toArray()

Array.from(iterable, mapfn)
Iterator(iterable).map(mapfn).toArray()

// Array.fromAsync

Array.fromAsync(asyncIterable)
AsyncIterator(asyncIterable).toArray()

Array.fromAsync(asyncIterable, mapfn)
AsyncIterator(asyncIterable).map(mapfn).toArray()

toArray overlaps with both Array.from and Array.fromAsync. This is okay. They can coexist. If we have to choose between having toArray and having fromAsync, then we should choose fromAsync. We already have Array.from. We should match the existing language precedent.

A co-champion of iterable-helpers agrees that we should have both or that we should prefer Array.fromAsync: “I remembered why it’s better for a buildable structure to consume an iterable than for an iterable to consume a buildable protocol. Sometimes building something one element at a time is the same as building it [more than one] element at a time, but sometimes it could be slow to build that way or produce a structure with equivalent semantics but different performance properties.”

TypedArray.fromAsync, Set.fromAsync, Object.fromEntriesAsync, etc.

The following built-ins also resemble Array.from:

TypedArray.from()
new Set
Object.fromEntries()
new Map

We are deferring any async versions of these methods to future proposals. See issue #8 and proposal-setmap-offrom.

Async spread operator

In the future, standardizing an async spread operator (like [ 0, await ...v ]) may be useful. This proposal leaves that idea to a separate proposal.

Records and tuples

The record/tuple proposal puts forward two new data types with APIs that respectively resemble those of Array and Object. The Tuple constructor, too, would probably need an fromAsync method. Whether the Record constructor gets a fromEntriesAsync method will depend on whether Object.fromEntriesAsync will also be added in a separate proposal.

Real-world examples

Only minor formatting changes have been made to the status-quo examples.

Status quo With Array.fromAsync
const all = require('it-all');

// Add the default assets to the repo.
const results = await all(
  addAll(
    globSource(initDocsPath, {
      recursive: true,
    }),
    { preload: false },
  ),
);
const dir = results
  .filter(file =>
    file.path === 'init-docs')
  .pop()
print('to get started, enter:\n');
print(
  `\tjsipfs cat` +
  `/ipfs/${dir.cid}/readme\n`,
);

From ipfs-core/src/runtime/init-assets-nodejs.js.

// Add the default assets to the repo.
const results = await Array.fromAsync(
  addAll(
    globSource(initDocsPath, {
      recursive: true,
    }),
    { preload: false },
  ),
);
const dir = results
  .filter(file =>
    file.path === 'init-docs')
  .pop()
print('to get started, enter:\n');
print(
  `\tjsipfs cat` +
  `/ipfs/${dir.cid}/readme\n`,
);
const all = require('it-all');

const results = await all(
  node.contentRouting
    .findProviders('a cid'),
);
expect(results)
  .to.be.an('array')
  .with.lengthOf(1)
  .that.deep.equals([result]);

From js-libp2p/test/content-routing/content-routing.node.js.

const results = await Array.fromAsync(
  node.contentRouting
    .findProviders('a cid'),
);
expect(results)
  .to.be.an('array')
  .with.lengthOf(1)
  .that.deep.equals([result]);
async function toArray(items) {
  const result = [];
  for await (const item of items) {
    result.push(item);
  }
  return result;
}

it('empty-pipeline', async () => {
  const pipeline = new Pipeline();
  const result = await toArray(
    pipeline.execute(
      [ 1, 2, 3, 4, 5 ]));
  assert.deepStrictEqual(
    result,
    [ 1, 2, 3, 4, 5 ],
  );
});

From node-httptransfer/test/generator/pipeline.test.js.

it('empty-pipeline', async () => {
  const pipeline = new Pipeline();
  const result = await Array.fromAsync(
    pipeline.execute(
      [ 1, 2, 3, 4, 5 ]));
  assert.deepStrictEqual(
    result,
    [ 1, 2, 3, 4, 5 ],
  );
});

More Repositories

1

proposals

Tracking ECMAScript Proposals
17,177
star
2

ecma262

Status, process, and documents for ECMA-262
HTML
14,437
star
3

proposal-pipeline-operator

A proposal for adding a useful pipe operator to JavaScript.
HTML
7,534
star
4

proposal-pattern-matching

Pattern matching syntax for ECMAScript
HTML
5,498
star
5

proposal-optional-chaining

HTML
4,942
star
6

proposal-type-annotations

ECMAScript proposal for type syntax that is erased - Stage 1
JavaScript
4,252
star
7

proposal-signals

A proposal to add signals to JavaScript.
3,387
star
8

proposal-temporal

Provides standard objects and functions for working with dates and times.
HTML
3,321
star
9

proposal-observable

Observables for ECMAScript
JavaScript
3,058
star
10

proposal-decorators

Decorators for ES6 classes
2,640
star
11

proposal-record-tuple

ECMAScript proposal for the Record and Tuple value types. | Stage 2: it will change!
HTML
2,496
star
12

test262

Official ECMAScript Conformance Test Suite
JavaScript
2,073
star
13

proposal-dynamic-import

import() proposal for JavaScript
HTML
1,863
star
14

proposal-bind-operator

This-Binding Syntax for ECMAScript
1,742
star
15

proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
HTML
1,722
star
16

proposal-async-await

Async/await for ECMAScript
HTML
1,578
star
17

proposal-object-rest-spread

Rest/Spread Properties for ECMAScript
HTML
1,493
star
18

proposal-shadowrealm

ECMAScript Proposal, specs, and reference implementation for Realms
HTML
1,429
star
19

proposal-iterator-helpers

Methods for working with iterators in ECMAScript
HTML
1,307
star
20

proposal-nullish-coalescing

Nullish coalescing proposal x ?? y
HTML
1,232
star
21

proposal-top-level-await

top-level `await` proposal for ECMAScript (stage 4)
HTML
1,083
star
22

proposal-partial-application

Proposal to add partial application to ECMAScript
HTML
1,002
star
23

proposal-do-expressions

Proposal for `do` expressions
HTML
990
star
24

proposal-binary-ast

Binary AST proposal for ECMAScript
961
star
25

agendas

TC39 meeting agendas
JavaScript
952
star
26

proposal-built-in-modules

HTML
891
star
27

proposal-async-iteration

Asynchronous iteration for JavaScript
HTML
857
star
28

proposal-explicit-resource-management

ECMAScript Explicit Resource Management
JavaScript
746
star
29

proposal-set-methods

Proposal for new Set methods in JS
HTML
655
star
30

proposal-string-dedent

TC39 Proposal to remove common leading indentation from multiline template strings
HTML
614
star
31

proposal-operator-overloading

JavaScript
610
star
32

proposal-import-attributes

Proposal for syntax to import ES modules with assertions
HTML
591
star
33

proposal-async-context

Async Context for JavaScript
HTML
587
star
34

proposal-bigint

Arbitrary precision integers in JavaScript
HTML
561
star
35

ecmascript_simd

SIMD numeric type for EcmaScript
JavaScript
540
star
36

ecma402

Status, process, and documents for ECMA 402
HTML
529
star
37

proposal-slice-notation

HTML
523
star
38

proposal-change-array-by-copy

Provides additional methods on Array.prototype and TypedArray.prototype to enable changes on the array by returning a new copy of it with the change.
HTML
511
star
39

notes

TC39 meeting notes
JavaScript
496
star
40

proposal-class-public-fields

Stage 2 proposal for public class fields in ECMAScript
HTML
489
star
41

proposal-iterator.range

A proposal for ECMAScript to add a built-in Iterator.range()
HTML
483
star
42

proposal-decimal

Built-in exact decimal numbers for JavaScript
HTML
477
star
43

proposal-uuid

UUID proposal for ECMAScript (Stage 1)
JavaScript
463
star
44

proposal-module-expressions

HTML
433
star
45

proposal-throw-expressions

Proposal for ECMAScript 'throw' expressions
JavaScript
425
star
46

proposal-UnambiguousJavaScriptGrammar

413
star
47

proposal-weakrefs

WeakRefs
HTML
409
star
48

proposal-array-grouping

A proposal to make grouping of array items easier
HTML
407
star
49

proposal-error-cause

TC39 proposal for accumulating errors
HTML
380
star
50

proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Shell
376
star
51

proposal-ecmascript-sharedmem

Shared memory and atomics for ECMAscript
HTML
374
star
52

proposal-module-declarations

JavaScript Module Declarations
HTML
369
star
53

proposal-first-class-protocols

a proposal to bring protocol-based interfaces to ECMAScript users
352
star
54

proposal-relative-indexing-method

A TC39 proposal to add an .at() method to all the basic indexable classes (Array, String, TypedArray)
HTML
351
star
55

proposal-global

ECMAScript Proposal, specs, and reference implementation for `global`
HTML
346
star
56

proposal-private-methods

Private methods and getter/setters for ES6 classes
HTML
345
star
57

proposal-numeric-separator

A proposal to add numeric literal separators in JavaScript.
HTML
330
star
58

proposal-private-fields

A Private Fields Proposal for ECMAScript
HTML
319
star
59

tc39.github.io

Get involved in specifying JavaScript
HTML
318
star
60

proposal-object-from-entries

TC39 proposal for Object.fromEntries
HTML
318
star
61

proposal-promise-allSettled

ECMAScript Proposal, specs, and reference implementation for Promise.allSettled
HTML
314
star
62

proposal-await.ops

Introduce await.all / await.race / await.allSettled / await.any to simplify the usage of Promises
HTML
310
star
63

proposal-regex-escaping

Proposal for investigating RegExp escaping for the ECMAScript standard
JavaScript
309
star
64

proposal-export-default-from

Proposal to add `export v from "mod";` to ECMAScript.
HTML
306
star
65

proposal-logical-assignment

A proposal to combine Logical Operators and Assignment Expressions
HTML
302
star
66

proposal-promise-finally

ECMAScript Proposal, specs, and reference implementation for Promise.prototype.finally
HTML
279
star
67

proposal-json-modules

Proposal to import JSON files as modules
HTML
272
star
68

proposal-asset-references

Proposal to ECMAScript to add first-class location references relative to a module
270
star
69

proposal-cancellation

Proposal for a Cancellation API for ECMAScript
HTML
267
star
70

proposal-promise-with-resolvers

HTML
255
star
71

proposal-string-replaceall

ECMAScript proposal: String.prototype.replaceAll
HTML
253
star
72

proposal-export-ns-from

Proposal to add `export * as ns from "mod";` to ECMAScript.
HTML
242
star
73

proposal-structs

JavaScript Structs: Fixed Layout Objects
230
star
74

proposal-ses

Draft proposal for SES (Secure EcmaScript)
HTML
223
star
75

proposal-intl-relative-time

`Intl.RelativeTimeFormat` specification [draft]
HTML
215
star
76

proposal-json-parse-with-source

Proposal for extending JSON.parse to expose input source text.
HTML
214
star
77

proposal-flatMap

proposal for flatten and flatMap on arrays
HTML
214
star
78

proposal-defer-import-eval

A proposal for introducing a way to defer evaluate of a module
HTML
208
star
79

ecmarkup

An HTML superset/Markdown subset source format for ECMAScript and related specifications
TypeScript
201
star
80

proposal-promise-any

ECMAScript proposal: Promise.any
HTML
200
star
81

proposal-optional-chaining-assignment

`a?.b = c` proposal
186
star
82

proposal-decorators-previous

Decorators for ECMAScript
HTML
184
star
83

proposal-smart-pipelines

Old archived draft proposal for smart pipelines. Go to the new Hack-pipes proposal at js-choi/proposal-hack-pipes.
HTML
181
star
84

proposal-upsert

ECMAScript Proposal, specs, and reference implementation for Map.prototype.upsert
HTML
176
star
85

proposal-collection-methods

HTML
171
star
86

proposal-array-filtering

A proposal to make filtering arrays easier
HTML
171
star
87

proposal-ptc-syntax

Discussion and specification for an explicit syntactic opt-in for Tail Calls.
HTML
169
star
88

proposal-extractors

Extractors for ECMAScript
JavaScript
166
star
89

proposal-error-stacks

ECMAScript Proposal, specs, and reference implementation for Error.prototype.stack / System.getStack
HTML
166
star
90

proposal-intl-duration-format

164
star
91

how-we-work

Documentation of how TC39 operates and how to participate
161
star
92

proposal-Array.prototype.includes

Spec, tests, reference implementation, and docs for ESnext-track Array.prototype.includes
HTML
157
star
93

proposal-promise-try

ECMAScript Proposal, specs, and reference implementation for Promise.try
HTML
154
star
94

proposal-extensions

Extensions proposal for ECMAScript
HTML
150
star
95

proposal-hashbang

#! for JS
HTML
148
star
96

proposal-import-meta

import.meta proposal for JavaScript
HTML
146
star
97

proposal-intl-segmenter

Unicode text segmentation for ECMAScript
HTML
146
star
98

proposal-resizablearraybuffer

Proposal for resizable array buffers
HTML
145
star
99

proposal-seeded-random

Proposal for an options argument to be added to JS's Math.random() function, and some options to start it with.
HTML
143
star
100

eshost

A uniform wrapper around a multitude of ECMAScript hosts. CLI: https://github.com/bterlson/eshost-cli
JavaScript
142
star