Skip to content

Commit 18917e4

Browse files
js-choiljharb
andauthored
explainer: Improve error-handling explanations (#21)
Closes #18. See also #16. Co-authored-by: Jordan Harband <[email protected]>
1 parent 32aa077 commit 18917e4

File tree

1 file changed

+121
-105
lines changed

1 file changed

+121
-105
lines changed

README.md

Lines changed: 121 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ 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
[§ Errors]: #errors
13+
[§ Sync-iterable inputs]: #sync-iterable-inputs
1314

1415
## Why an Array.fromAsync method
1516
Since its standardization in JavaScript, **[Array.from][]** has become one of
@@ -81,9 +82,8 @@ const arr = await Array.fromAsync(asyncGen(4));
8182
If the argument is a sync iterable (and not an async iterable), then the return
8283
value is still a promise that will resolve to an array. If the sync iterator
8384
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`.
8787

8888
[Zalgo]: https://blog.izs.me/2013/08/designing-apis-for-asynchrony/
8989

@@ -103,11 +103,50 @@ for await (const v of genPromises(4)) {
103103
const arr = await Array.fromAsync(genPromises(4));
104104
```
105105

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+
106133
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+
```
111150

112151
### Non-iterable array-like inputs
113152
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)) {
142181
const arr = await Array.fromAsync(arrLike);
143182
```
144183

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][]).
149188

150189
### Generic factory method
151190
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
213252
promise. Array.fromAsync will never synchronously throw an error and [summon
214253
Zalgo][Zalgo].
215254

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
217256
iterator, then Array.fromAsync’s returned promise will reject with that error.
218257

219258
```js
220259
const err = new Error;
221260
const badIterable = { [Symbol.iterator] () { throw err; } };
222261

223-
// This creates a promise that will reject with `err`.
262+
// This returns a promise that will reject with `err`.
224263
Array.fromAsync(badIterable);
225264
```
226265

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
228267
iterating, then Array.fromAsync’s returned promise will reject with that error.
229268

230269
```js
231270
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; }
237272

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`.
243274
Array.fromAsync(genErrorAsync());
244275
```
245276

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-
250277
```js
251278
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; }
262280

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());
273283
```
274284

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.
279289

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.
282293

283294
```js
284295
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);
290298
}
291299

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.
294302
Array.fromAsync(genZeroThenRejection());
295303
```
296304

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.
309308

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.
314320

315321
```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+
});
321330
}
322331

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+
}
327338

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() ]);
339355
```
340356

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.
344360

345361
```js
346362
const err = new Error;
347363
function badCallback () { throw err; }
348364

349-
// This creates a promise that will reject with `err`.
365+
// This returns a promise that will reject with `err`.
350366
Array.fromAsync([ 0 ], badCallback);
351367
```
352368

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.
356372

357373
```js
358-
// These create promises that will reject with TypeErrors.
374+
// These return promises that will reject with TypeErrors.
359375
Array.fromAsync(null);
360376
Array.fromAsync([], 1);
361377
```

0 commit comments

Comments
 (0)