Skip to content

Commit 299ac6b

Browse files
committed
explainer: Reformatting
Hard wrap lines. Consistently use function names without formatting. Interleave error code examples with explanations. Add separation lines within code blocks. Link to Zalgo article and comment sources. Remove note about plenary discussion of array-likes. Make more reference links.
1 parent 20ea6a7 commit 299ac6b

File tree

1 file changed

+169
-76
lines changed

1 file changed

+169
-76
lines changed

README.md

Lines changed: 169 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# `Array.fromAsync` for JavaScript
1+
# Array.fromAsync for JavaScript
22
ECMAScript Stage-2 Proposal. J. S. Choi, 2021.
33

44
* **[Specification][]** available
@@ -10,19 +10,17 @@ ECMAScript Stage-2 Proposal. J. S. Choi, 2021.
1010
[core-js]: https://github.com/zloirock/core-js#arrayfromasync
1111
[array-from-async]: https://www.npmjs.com/package/array-from-async
1212

13-
## Why an `Array.fromAsync` method
14-
Since its standardization in JavaScript,
15-
**[`Array.from`][]** has become one of `Array`’s
16-
most frequently used built-in methods.
17-
However, no similar functionality exists for async iterators.
13+
## Why an Array.fromAsync method
14+
Since its standardization in JavaScript, **[Array.from][]** has become one of
15+
`Array`’s most frequently used built-in methods. However, no similar
16+
functionality exists for async iterators.
1817

19-
[`Array.from`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from
18+
[Array.from]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from
2019

21-
Such functionality would be useful
22-
for **dumping** the entirety of an **async iterator**
23-
into a **single data structure**,
24-
especially in **unit tests** or in **command-line** interfaces.
25-
(Several real-world examples are included in a following section.)
20+
Such functionality would be useful for **dumping** the entirety of an **async
21+
iterator** into a **single data structure**, especially in **unit tests** or in
22+
**command-line** interfaces. (Several real-world examples are included in a
23+
following section.)
2624

2725
There is an [it-all][] NPM library that performs only this task
2826
and which gets about 50,000 weekly downloads daily.
@@ -47,54 +45,75 @@ later in this explainer.
4745
## Description
4846
(A [formal draft specification][specification] is available.)
4947

50-
### Async-iterable inputs
51-
Similarly to **[`Array.from`][]**,
52-
**`Array.fromAsync`** would be a **static method**
53-
of the `Array` built-in class, with **one required argument**
54-
and **two optional arguments**: `(items, mapfn, thisArg)`.
48+
Array.fromAsync is to `for await`\
49+
as Array.from is to `for`.
50+
51+
Similarly to [Array.from][], Array.fromAsync would be a static method of the
52+
`Array` built-in class, with one required argument and two optional arguments:
53+
`(items, mapfn, thisArg)`.
5554

56-
But instead of converting an **array-like object** or **iterable** to an array,
57-
it converts an **async iterable** (or array-like object or iterable)
58-
to a **promise** that will resolve to an array.
55+
### Async-iterable inputs
56+
But, instead of converting a sync iterable to an array, Array.fromAsync can
57+
convert an async iterable to a **promise** that (if everything goes well) will
58+
resolve to a new array. Before the promise resolves, it will create an async
59+
iterator from the input, lazily iterate over it, and add each yielded value to
60+
the new array. (The promise is immediately returned after the Array.fromAsync
61+
function call, no matter what.)
5962

6063
```js
6164
async function * asyncGen (n) {
6265
for (let i = 0; i < n; i++)
6366
yield i * 2;
6467
}
65-
// arr will be [0, 2, 4, 6].
68+
69+
// `arr` will be `[0, 2, 4, 6]`.
6670
const arr = [];
6771
for await (const v of asyncGen(4)) {
6872
arr.push(v);
6973
}
74+
7075
// This is equivalent.
7176
const arr = await Array.fromAsync(asyncGen(4));
7277
```
7378

7479
### Sync-iterable inputs
75-
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.
76-
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 for one microtick to prevent Zalgo.)
77-
This matches the behavior of `for await`.
80+
If the argument is a sync iterable (and not an async iterable), then the return
81+
value is still a promise that will resolve to an array. If the sync iterator
82+
yields promises, then each yielded promise is awaited before its value is added
83+
to the new array. (Values that are not promises are also awaited for one
84+
microtick to [prevent Zalgo][Zalgo].) All of this matches the behavior of `for
85+
await`.
86+
87+
[Zalgo]: https://blog.izs.me/2013/08/designing-apis-for-asynchrony/
7888

7989
```js
8090
function * genPromises (n) {
8191
for (let i = 0; i < n; i++)
8292
yield Promise.resolve(i * 2);
8393
}
84-
// arr will be [0, 2, 4, 6].
94+
95+
// `arr` will be `[ 0, 2, 4, 6 ]`.
8596
const arr = [];
8697
for await (const v of genPromises(4)) {
8798
arr.push(v);
8899
}
100+
89101
// This is equivalent.
90102
const arr = await Array.fromAsync(genPromises(4));
91103
```
92104

93105
### Non-iterable array-like inputs
94-
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.
95-
The return value is still a promise that will resolve to an array.
96-
If the array-like object’s elements are promises, then each accessed promise is awaited before its value is added to the new array.
97-
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.”
106+
Array.fromAsync’s valid inputs are a superset of Array.from’s valid inputs. This
107+
includes non-iterable array-likes: objects that have a length property as well
108+
as indexed elements. The return value is still a promise that will resolve to an
109+
array. If the array-like object’s elements are promises, then each accessed
110+
promise is awaited before its value is added to the new array.
111+
112+
One [TC39 representative’s opinion][issue #7 comment]: “[Array-likes are] very
113+
much not obsolete, and it’s very nice that things aren’t forced to implement the
114+
iterator protocol to be transformable into an Array.”
115+
116+
[issue #7 comment]: https://github.com/tc39/proposal-array-from-async/issues/7#issuecomment-920299880
98117

99118
```js
100119
const arrLike = {
@@ -104,22 +123,26 @@ const arrLike = {
104123
2: Promise.resolve(4),
105124
3: Promise.resolve(6),
106125
}
107-
// arr will be [0, 2, 4, 6].
126+
127+
// `arr` will be `[ 0, 2, 4, 6 ]`.
108128
const arr = [];
109129
for await (const v of Array.from(arrLike)) {
110130
arr.push(v);
111131
}
132+
112133
// This is equivalent.
113134
const arr = await Array.fromAsync(arrLike);
114-
See issue #7. Previously discussed at 2021-11 plenary without objections.
115135
```
116136

117137
### Generic factory method
118-
Array.fromAsync is a generic factory method. It does not require that its this receiver be the Array constructor.
119-
fromAsync can be transferred to or inherited by any other constructor with a single numeric parameter. In that case, the final result will be the data structure created by that constructor (with 0 as its argument), and with each value yielded by the input being assigned to the data structure’s numeric properties.
120-
(Symbol.species is not involved at all.)
121-
If the this receiver is not a constructor, then fromAsync creates an array as usual.
122-
This matches the behavior of Array.from.
138+
Array.fromAsync is a generic factory method. It does not require that its this
139+
receiver be the Array constructor. fromAsync can be transferred to or inherited
140+
by any other constructor with a single numeric parameter. In that case, the
141+
final result will be the data structure created by that constructor (with 0 as
142+
its argument), and with each value yielded by the input being assigned to the
143+
data structure’s numeric properties. (Symbol.species is not involved at all.) If
144+
the this receiver is not a constructor, then fromAsync creates an array as
145+
usual. This matches the behavior of Array.from.
123146

124147
```js
125148
async function * asyncGen (n) {
@@ -129,61 +152,120 @@ async function * asyncGen (n) {
129152
function Data (n) {}
130153
Data.from = Array.from;
131154
Data.fromAsync = Array.fromAsync;
132-
// d will be a new Data(0), with
133-
// 0 assigned to 0, 1 assigned to 2, etc.
155+
156+
// d will be a `new Data(0)`, with its `0` property assigned to `0`, its `1`
157+
// property assigned to `2`, etc.
134158
const d = new Data(0); let i = 0;
135159
for await (const v of asyncGen(4)) {
136160
d[i] = v;
137161
}
162+
138163
// This is equivalent.
139164
const d = await Data.fromAsync(asyncGen(4));
140165
```
141166

142167
### Optional parameters
143-
Array.fromAsync has two optional parameters.
144-
The first optional parameter is a mapping callback, which is called on each value yielded from the input – the result of which is awaited then added to the array.
145-
Unlike `Array.from`, `mapfn` may be an async function.)
146-
By default, this is essentially an identity function.
147-
The second optional parameter is a this value for the mapping callback. By default, this is undefined.
148-
These optional parameters match the behavior of Array.from. Their exclusion would be surprising to developers who are already used to Array.from.
168+
Array.fromAsync has two optional parameters: `mapfn` and `thisArg`.
169+
170+
`mapfn` is a mapping callback, which is called on each value yielded from the
171+
input – the result of which is awaited then added to the array. Unlike
172+
Array.from, `mapfn` may be an async function.) By default, this is essentially
173+
an identity function.
174+
175+
`thisArg` is a `this`-binding receiver value for the mapping callback. By
176+
default, this is undefined. These optional parameters match the behavior of
177+
Array.from. Their exclusion would be surprising to developers who are already
178+
used to Array.from.
149179

150180
```js
151181
async function * asyncGen (n) {
152182
for (let i = 0; i < n; i++)
153183
yield i * 2;
154184
}
155-
// arr will be [0, 4, 16, 36].
185+
186+
// `arr` will be `[ 0, 4, 16, 36 ]`.
156187
const arr = [];
157188
for await (const v of asyncGen(4)) {
158189
arr.push(v ** 2);
159190
}
191+
160192
// This is equivalent.
161-
const arr = await Array.fromAsync(asyncGen(4),
162-
v => v ** 2);
193+
const arr = await Array.fromAsync(asyncGen(4), v =>
194+
v ** 2);
163195
```
164196

165197
### Errors
166-
Like other promise-based APIs, Array.fromAsync will always immediately return a promise. It will never synchronously throw an error and summon Zalgo.
167-
If its input throws an error while creating its async or sync iterator, then its promise will reject with that error.
168-
If its input’s iterator throws an error while yielding a value, then its promise will reject with that error.
169-
If its this receiver’s constructor throws an error, then its promise will reject to that error.
170-
If its mapping callback throws an error when given an input value, then its promise will reject with that error.
171-
If its input is null or undefined, or if its mapping callback is neither undefined nor callable, then its promise will reject with a TypeError.
198+
Like other promise-based APIs, Array.fromAsync will always immediately return a
199+
promise. Array.fromAsync will never synchronously throw an error and [summon
200+
Zalgo][Zalgo].
201+
202+
If Array.fromAsync’s input throws an error while creating its async or sync
203+
iterator, then Array.fromAsync’s returned promise will reject with that error.
172204

173205
```js
174206
const err = new Error;
175207
const badIterable = { [Symbol.iterator] () { throw err; } };
176-
function * genError () { throw err; }
177-
function * genRejection () { yield Promise.reject(err); }
178-
function badCallback () { throw err; }
179-
function BadConstructor () { throw err; }
180-
// These create promises that will reject with err.
208+
209+
// This creates a promise that will reject with `err`.
181210
Array.fromAsync(badIterable);
211+
```
212+
213+
If Array.fromAsync’s input is iterable but the input’s iterator throws while
214+
iterating, then Array.fromAsync’s returned promise will reject with that error.
215+
216+
```js
217+
const err = new Error;
218+
function * genError () { throw err; }
219+
220+
// This creates a promise that will reject with `err`.
182221
Array.fromAsync(genError());
183-
Array.fromAsync(genRejection());
222+
```
223+
224+
```js
225+
const err = new Error;
226+
function * genErrorAsync () { throw err; }
227+
228+
// This creates a promise that will reject with `err`.
184229
Array.fromAsync(genErrorAsync());
185-
Array.fromAsync([1], badCallback);
186-
BadConstructor.call(Array.fromAsync, []);
230+
```
231+
232+
If Array.fromAsync’s input is sync (i.e., the input is not an async iterable),
233+
and if one of the input’s values is a promise that eventually rejects or has
234+
rejected, then Array.fromAsync’s returned promise will reject with that error.
235+
236+
```js
237+
const err = new Error;
238+
function * genRejection () { yield Promise.reject(err); }
239+
240+
// This creates a promise that will reject with `err`.
241+
Array.fromAsync(genRejection());
242+
```
243+
244+
```js
245+
const err = new Error;
246+
const arrLikeWithRejection = { length: 1, 0: Promise.reject(err) };
247+
248+
// This creates a promise that will reject with `err`.
249+
Array.fromAsync(arrLikeWithRejection);
250+
```
251+
252+
If Array.fromAsync’s input has at least one value, and Array.fromAsync’s mapping
253+
callback throws an error when given any of those values, then Array.fromAsync’s
254+
returned promise will reject with the first such error.
255+
256+
```js
257+
const err = new Error;
258+
function badCallback () { throw err; }
259+
260+
// This creates a promise that will reject with `err`.
261+
Array.fromAsync([ 0 ], badCallback);
262+
```
263+
264+
If Array.fromAsync’s input is null or undefined, or if Array.fromAsync’s mapping
265+
callback is neither undefined nor callable, then Array.fromAsync’s returned
266+
promise will reject with a TypeError.
267+
268+
```js
187269
// These create promises that will reject with TypeErrors.
188270
Array.fromAsync(null);
189271
Array.fromAsync([], 1);
@@ -192,7 +274,8 @@ Array.fromAsync([], 1);
192274
## Other proposals
193275

194276
### Relationship with iterator-helpers
195-
The [iterator-helpers][] proposal has toArray, which works with both sync and async iterables.
277+
The [iterator-helpers][] proposal has toArray, which works with both sync and
278+
async iterables.
196279

197280
```js
198281
Array.from(gen())
@@ -201,36 +284,46 @@ Array.fromAsync(asyncGen())
201284
asyncGen().toArray()
202285
```
203286

204-
toArray overlaps with both Array.from and Array.fromAsync. This is okay. They can coexist.
205-
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.
287+
toArray overlaps with both Array.from and Array.fromAsync. This is okay. They
288+
can coexist. If we have to choose between having toArray and having fromAsync,
289+
then we should choose fromAsync. We already have Array.from. We should match the
290+
existing language precedent.
206291

207-
See [tc39/proposal-iterator-helpers#156](https://github.com/tc39/proposal-iterator-helpers/issues/156).
208-
A co-champion of iterable-helpers seems to agree that we should have both or that we should prefer Array.fromAsync:
209-
“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.”
292+
A [co-champion of iterable-helpers agrees][tc39/proposal-iterator-helpers#156]
293+
that we should have both or that we should prefer Array.fromAsync: “I remembered
294+
why it’s better for a buildable structure to consume an iterable than for an
295+
iterable to consume a buildable protocol. Sometimes building something one
296+
element at a time is the same as building it [more than one] element at a time,
297+
but sometimes it could be slow to build that way or produce a structure with
298+
equivalent semantics but different performance properties.”
210299

211300
[iterator-helpers]: https://github.com/tc39/proposal-iterator-helpers
301+
[tc39/proposal-iterator-helpers#156]: https://github.com/tc39/proposal-iterator-helpers/issues/156.
212302

213-
### TypedArray.fromAsync, Set.fromAsync, etc.
303+
### TypedArray.fromAsync, Set.fromAsync, Object.fromEntriesAsync, etc.
214304
The following built-ins also resemble Array.from:
215305
```js
216-
TypedArray.from
306+
TypedArray.from()
217307
new Set
218-
Object.fromEntries
308+
Object.fromEntries()
219309
new Map
220310
```
221311
We are deferring any async versions of these methods to future proposals.
222-
See [issue #8](https://github.com/tc39/proposal-array-from-async/issues/8) and [proposal-setmap-offrom](https://github.com/tc39/proposal-setmap-offrom).
312+
See [issue #8][] and [proposal-setmap-offrom][].
313+
314+
[issue #8]: https://github.com/tc39/proposal-array-from-async/issues/8
315+
[proposal-setmap-offrom]: https://github.com/tc39/proposal-setmap-offrom
223316

224317
### Async spread operator
225318
In the future, standardizing an async spread operator (like `[ 0, await ...v ]`)
226319
may be useful. This proposal leaves that idea to a **separate** proposal.
227320

228321
### Records and tuples
229-
The **[record/tuple] proposal** puts forward two new data types
230-
with APIs that respectively **resemble** those of **`Array` and `Object`**.
231-
The `Tuple` constructor, too, would probably need an `fromAsync` method.
232-
Whether the `Record` constructor gets a `fromEntriesAsync` method
233-
depends on [whether `Object` gets `fromEntriesAsync`](#objectfromentriesasync).
322+
The **[record/tuple] proposal** puts forward two new data types with APIs that
323+
respectively **resemble** those of **`Array` and `Object`**. The `Tuple`
324+
constructor, too, would probably need an `fromAsync` method. Whether the
325+
`Record` constructor gets a `fromEntriesAsync` method will depend on whether
326+
`Object.fromEntriesAsync` will also be added in a separate proposal.
234327

235328
[record/tuple]: https://github.com/tc39/proposal-record-tuple
236329

0 commit comments

Comments
 (0)