@@ -10,6 +10,7 @@ ECMAScript Stage-2 Proposal. J. S. Choi, 2021.
10
10
[ core-js ] : https://github.com/zloirock/core-js#arrayfromasync
11
11
[ array-from-async ] : https://www.npmjs.com/package/array-from-async
12
12
[ § Errors ] : #errors
13
+ [ § Sync-iterable inputs ] : #sync-iterable-inputs
13
14
14
15
## Why an Array.fromAsync method
15
16
Since its standardization in JavaScript, ** [ Array.from] [ ] ** has become one of
@@ -81,9 +82,8 @@ const arr = await Array.fromAsync(asyncGen(4));
81
82
If the argument is a sync iterable (and not an async iterable), then the return
82
83
value is still a promise that will resolve to an array. If the sync iterator
83
84
yields promises, then each yielded promise is awaited before its value is added
84
- to the new array. (Values that are not promises are also awaited for one
85
- microtick to [ prevent Zalgo] [ Zalgo ] .) All of this matches the behavior of `for
86
- await`.
85
+ to the new array. (Values that are not promises are also awaited to
86
+ [ prevent Zalgo] [ Zalgo ] .) All of this matches the behavior of ` for await ` .
87
87
88
88
[ Zalgo ] : https://blog.izs.me/2013/08/designing-apis-for-asynchrony/
89
89
@@ -103,11 +103,50 @@ for await (const v of genPromises(4)) {
103
103
const arr = await Array .fromAsync (genPromises (4 ));
104
104
```
105
105
106
+ Like ` for await ` , Array.fromAsync ** lazily** iterates over a sync-but-not-async
107
+ input. Whenever a developer needs to dump a synchronous input that yields
108
+ promises into an array, the developer needs to choose carefully between
109
+ Array.fromAsync and Promise.all, which have complementary control flows:
110
+
111
+ <table >
112
+ <thead >
113
+ <tr>
114
+ <th></th>
115
+ <th>Parallel awaiting</th>
116
+ <th>Sequential awaiting</th>
117
+ </tr>
118
+ </thead >
119
+ <tbody >
120
+ <tr>
121
+ <th>Lazy iteration</th>
122
+ <td>Impossible</td>
123
+ <td><code>await Array.fromAsync(input)</code></td>
124
+ </tr>
125
+ <tr>
126
+ <th>Eager iteration</th>
127
+ <td><code>await Promise.all(Array.from(input))</code></td>
128
+ <td>Useless</td>
129
+ </tr>
130
+ </tbody >
131
+ </table >
132
+
106
133
Also like ` for await ` , when given a sync-but-not-async iterable input, then
107
- Array.fromAsync ** will not handle** any yielded promises that ** reject** .
108
- This is because ` Array.fromAsync ` ** lazily** iterates over its input, so it
109
- cannot attach any handlers to its input’s promise values. For more information,
110
- see [ § Errors] [ ] .
134
+ Array.fromAsync will catch ** only** the first rejection that its iteration
135
+ reaches, and only if that rejection does ** not** occur in a microtask before the
136
+ iteration reaches and awaits for it. For more information, see [ § Errors] [ ] .
137
+
138
+ ``` js
139
+ // `arr` will be `[ 0, 2, 4, 6 ]`.
140
+ // `genPromises(4)` is lazily iterated,
141
+ // and its four yielded promises are awaited in sequence.
142
+ const arr = await Array .fromAsync (genPromises (4 ));
143
+
144
+ // `arr` will also be `[ 0, 2, 4, 6 ]`.
145
+ // However, `genPromises(4)` is eagerly iterated
146
+ // (into an array of four promises),
147
+ // and the four promises are awaited in parallel.
148
+ const arr = await Promise .all (Array .from (genPromises (4 )));
149
+ ```
111
150
112
151
### Non-iterable array-like inputs
113
152
Array.fromAsync’s valid inputs are a superset of Array.from’s valid inputs. This
@@ -142,10 +181,10 @@ for await (const v of Array.from(arrLike)) {
142
181
const arr = await Array .fromAsync (arrLike);
143
182
```
144
183
145
- As with sync iterables, when given a non- iterable input, then Array.fromAsync
146
- ** will not handle ** any yielded promises that ** reject ** . This is because
147
- ` Array.fromAsync ` ** lazily ** iterates over its input, so it cannot attach any
148
- handlers to its input’s promise values. For more information, see [ § Errors] [ ] .
184
+ As it does with sync-but-not-async iterable inputs, Array.fromAsync lazily
185
+ iterates over the values of array-like inputs, and it awaits each value.
186
+ The developer must choose between using Array.fromAsync and Promise.all (see
187
+ [ § Sync-iterable inputs ] ( #sync-iterable-inputs ) and [ § Errors] [ ] ) .
149
188
150
189
### Generic factory method
151
190
Array.fromAsync is a generic factory method. It does not require that its this
@@ -213,149 +252,126 @@ Like other promise-based APIs, Array.fromAsync will always immediately return a
213
252
promise. Array.fromAsync will never synchronously throw an error and [ summon
214
253
Zalgo] [ Zalgo ] .
215
254
216
- If Array.fromAsync’s input throws an error while creating its async or sync
255
+ When Array.fromAsync’s input throws an error while creating its async or sync
217
256
iterator, then Array.fromAsync’s returned promise will reject with that error.
218
257
219
258
``` js
220
259
const err = new Error ;
221
260
const badIterable = { [Symbol .iterator ] () { throw err; } };
222
261
223
- // This creates a promise that will reject with `err`.
262
+ // This returns a promise that will reject with `err`.
224
263
Array .fromAsync (badIterable);
225
264
```
226
265
227
- If Array.fromAsync’s input is iterable but the input’s iterator throws while
266
+ When Array.fromAsync’s input is iterable but the input’s iterator throws while
228
267
iterating, then Array.fromAsync’s returned promise will reject with that error.
229
268
230
269
``` js
231
270
const err = new Error ;
232
- function * genError () { throw err; }
233
-
234
- // This creates a promise that will reject with `err`.
235
- Array .fromAsync (genError ());
236
- ```
271
+ async function * genErrorAsync () { throw err; }
237
272
238
- ``` js
239
- const err = new Error ;
240
- function * genErrorAsync () { throw err; }
241
-
242
- // This creates a promise that will reject with `err`.
273
+ // This returns a promise that will reject with `err`.
243
274
Array .fromAsync (genErrorAsync ());
244
275
```
245
276
246
- If Array.fromAsync’s input is sync (i.e., the input is not an async iterable),
247
- and if one of the input’s values is a promise that eventually rejects or has
248
- rejected, then Array.fromAsync’s returned promise will reject with that error.
249
-
250
277
``` js
251
278
const err = new Error ;
252
- const rejection = Promise .reject (err);
253
- function * genZeroThenRejection () {
254
- yield 0 ;
255
- yield rejection;
256
- }
257
-
258
- // This creates a promise that will reject with `err`. However, `rejection`
259
- // itself will not be handled by Array.fromAsync.
260
- Array .fromAsync (genZeroThenRejection ());
261
- ```
279
+ function * genError () { throw err; }
262
280
263
- ``` js
264
- const err = new Error ;
265
- const arrLikeWithRejection = {
266
- length: 2 ,
267
- 0 : 0 ,
268
- 1 : Promise .reject (err),
269
- };
270
-
271
- // This creates a promise that will reject with `err`.
272
- Array .fromAsync (arrLikeWithRejection);
281
+ // This returns a promise that will reject with `err`.
282
+ Array .fromAsync (genError ());
273
283
```
274
284
275
- However, like ` for await ` , in this case Array.fromAsync ** will not handle ** any
276
- yielded rejecting promises. This is because ` Array.fromAsync ` ** lazily **
277
- iterates over its input, so it cannot attach any handlers to its input ’s promise
278
- values .
285
+ When Array.fromAsync’s input is synchronous only (i.e., the input is not an
286
+ async iterable), and when one of the input’s values is a promise that eventually
287
+ rejects or has rejected, then iteration stops and Array.fromAsync ’s returned
288
+ promise will reject with the first such error .
279
289
280
- The creator of the rejecting promise is expected to synchronously attach a
281
- rejection handler when the promise is created, as usual:
290
+ In this case, Array.fromAsync will catch and handle that first input rejection
291
+ ** only if** that rejection does ** not** occur in a microtask before the
292
+ iteration reaches and awaits for it.
282
293
283
294
``` js
284
295
const err = new Error ;
285
- // The creator of the rejecting promise attaches a rejection handler.
286
- const rejection = Promise .reject (err).catch (console .error );
287
- function * genZeroThenRejection () {
288
- yield 0 ;
289
- yield rejection;
296
+ function * genRejection () {
297
+ yield Promise .reject (err);
290
298
}
291
299
292
- // This still creates a promise that will reject with `err`. `err` will also
293
- // separately be printed to the console due to the rejection handler .
300
+ // This returns a promise that will reject with `err`. There is **no** unhandled
301
+ // promise rejection, because the rejection occurs in the same microtask .
294
302
Array .fromAsync (genZeroThenRejection ());
295
303
```
296
304
297
- ``` js
298
- const err = new Error ;
299
- const arrLikeWithRejection = {
300
- length: 2 ,
301
- 0 : 0 ,
302
- 1 : Promise .reject (err),
303
- };
304
-
305
- // This still creates a promise that will reject with `err`. `err` will also
306
- // separately be printed to the console due to the rejection handler.
307
- Array .fromAsync (arrLikeWithRejection);
308
- ```
305
+ Just like with ` for await ` , Array.fromAsync will ** not** catch any rejections by
306
+ the input’s promises whenever those rejections occur ** before** the ticks in
307
+ which Array.fromAsync’s iteration reaches those promises.
309
308
310
- Alternatively, the user of the promises can switch from Array.fromAsync to
311
- Promise.all. Promise.all would change the control flow from lazy sync iteration
312
- (with sequential awaiting) to eager sync iteration (with parallel awaiting),
313
- allowing the handling of any rejection in the input.
309
+ This is because – like ` for await ` – Array.fromAsync ** lazily** iterates over
310
+ its input and ** sequentially** awaits each yielded value. Whenever a developer
311
+ needs to dump a synchronous input that yields promises into an array, the
312
+ developer needs to choose carefully between Array.fromAsync and Promise.all,
313
+ which have complementary control flows (see [ § Sync-iterable
314
+ inputs] ( #sync-iterable-inputs ) ).
315
+
316
+ For example, when a synchronous input contains two promises, the latter of which
317
+ will reject before the former promise resolves, then Array.fromAsync will not
318
+ catch that rejection, because it lazily reaches the rejecting promise only after
319
+ it already has rejected.
314
320
315
321
``` js
316
- const err = new Error ;
317
- const rejection = Promise .reject (err);
318
- function * genZeroThenRejection () {
319
- yield 0 ;
320
- yield rejection;
322
+ const numOfMillisecondsPerSecond = 1000 ;
323
+ const slowError = new Error ;
324
+ const fastError = new Error ;
325
+
326
+ function waitThenReject (value ) {
327
+ return new Promise ((resolve , reject ) => {
328
+ setTimeout (() => reject (value), numOfMillisecondsPerSecond);
329
+ });
321
330
}
322
331
323
- // Creates a promise that will reject with `err`. Unlike Array.fromAsync,
324
- // Promise.all will handle the `rejection`.
325
- Promise .all (genZeroThenRejection ());
326
- ```
332
+ function * genRejections () {
333
+ // Slow promise.
334
+ yield waitAndReject (slowError);
335
+ // Fast promise.
336
+ yield Promise .reject (fastError);
337
+ }
327
338
328
- ``` js
329
- const err = new Error ;
330
- const arrLikeWithRejection = {
331
- length: 2 ,
332
- 0 : 0 ,
333
- 1 : Promise .reject (err),
334
- };
335
-
336
- // Creates a promise that will reject with `err`. Unlike Array.fromAsync,
337
- // Promise.all will handle the `rejection`.
338
- Promise .all (Array .from (arrLikeWithRejection));
339
+ // This returns a promise that will reject with `slowError`. There is **no**
340
+ // unhandled promise rejection: the iteration is lazy and will stop early at the
341
+ // slow promise, so the fast promise will never be created.
342
+ Array .fromAsync (genSlowRejectThenFastReject ());
343
+
344
+ // This returns a promise that will reject with `slowError`. There **is** an
345
+ // unhandled promise rejection with `fastError`: the iteration eagerly creates
346
+ // and dumps both promises into an array, but Array.fromAsync will
347
+ // **sequentially** handle only the slow promise.
348
+ Array .fromAsync ([ ... genSlowRejectThenFastReject () ]);
349
+
350
+ // This returns a promise that will reject with `fastError`. There is **no**
351
+ // unhandled promise rejection: the iteration eagerly creates and dumps both
352
+ // promises into an array, but Promise.all will handle both promises **in
353
+ // parallel**.
354
+ Promise .all ([ ... genSlowRejectThenFastReject () ]);
339
355
```
340
356
341
- If Array.fromAsync’s input has at least one value, and Array.fromAsync’s mapping
342
- callback throws an error when given any of those values, then Array.fromAsync’s
343
- returned promise will reject with the first such error.
357
+ When Array.fromAsync’s input has at least one value, and when Array.fromAsync’s
358
+ mapping callback throws an error when given any of those values, then
359
+ Array.fromAsync’s returned promise will reject with the first such error.
344
360
345
361
``` js
346
362
const err = new Error ;
347
363
function badCallback () { throw err; }
348
364
349
- // This creates a promise that will reject with `err`.
365
+ // This returns a promise that will reject with `err`.
350
366
Array .fromAsync ([ 0 ], badCallback);
351
367
```
352
368
353
- If Array.fromAsync’s input is null or undefined, or if Array.fromAsync’s mapping
354
- callback is neither undefined nor callable, then Array.fromAsync’s returned
355
- promise will reject with a TypeError.
369
+ When Array.fromAsync’s input is null or undefined, or when Array.fromAsync’s
370
+ mapping callback is neither undefined nor callable, then Array.fromAsync’s
371
+ returned promise will reject with a TypeError.
356
372
357
373
``` js
358
- // These create promises that will reject with TypeErrors.
374
+ // These return promises that will reject with TypeErrors.
359
375
Array .fromAsync (null );
360
376
Array .fromAsync ([], 1 );
361
377
```
0 commit comments