proposal-array-from-async: What happens with rejections in synchronous iterables?
Are synchronous iterables/array-likes being iterated synchronously or asynchronously as they would in a for await? What is the expectation for a rejection within a sync collection that settles before to other promises prior? In a for await it goes unhandled. Would Array.fromAsync() be able to account for that and reject its returned promise or would it to allow for the unhandled rejection to slip through?
const delay = (ms, reject = false) => new Promise((r, j) => setTimeout(reject ? j : r, ms, ms));
for await (let ms of [delay(1000), delay(3000), delay(2000, true)]) {
console.log(ms) // 1000, (unhandled error 2000), 3000
}
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Comments: 19 (16 by maintainers)
Commits related to this issue
- explainer: Lack of eager rejection handling Closes #16. — committed to tc39/proposal-array-from-async by js-choi 3 years ago
- explainer: Improve error-handling explanations Closes #18. See also #16. — committed to tc39/proposal-array-from-async by js-choi 2 years ago
- explainer: Improve error-handling explanations Closes #18. See also #16. Co-authored-by: Jordan Harband <ljharb@gmail.com> — committed to tc39/proposal-array-from-async by js-choi 2 years ago
- explainer: Improve error-handling explanations (#21) Closes #18. See also #16. Co-authored-by: Jordan Harband <ljharb@gmail.com> — committed to tc39/proposal-array-from-async by js-choi 2 years ago
As I mentioned in https://github.com/tc39/proposal-array-from-async/issues/16#issuecomment-995463592, I agree with @ljharb and @mhofman. I think we should continue to use Async-from-Sync iterators (i.e.,
for awaitsemantics).The current semantics are such that, if
b()will reject beforea()resolves, thenArray.fromAsync((function * () { yield a(); yield b(); })())will still eventually return a promise that will eventually reject withb()’s error. However,b’s error intentionally cannot be caught byfromAsyncdue to its lazy iteration, which makes simultaneously awaiting promises, in parallel, impossible.The expectation is that, as usual, the creator of the
b()promise will catchb()’s rejection whereverb()is created (e.g., atyield b()).Array.fromAsyncalready resemblesfor awaitin nearly every way, just asArray.fromresemblesfor, and we want to continue this analogy. Like @ljharb says,Array.fromAsyncis not intended to be a promise combinator, which would defeat its main purpose for async iterators—instead, it is an operation on potentially lazy iterators. The lazy iterators can be lazy async iterators or lazy sync iterators, but, either way, developers’ mental model is that they are lazy.Moreover, eager iteration then parallel awaiting is already possible with
Promise.all.The choice between “eager iteration then parallel awaiting” versus “lazy iteration with sequential awaiting” is a crucial and fundamental concern of control flow, and the developer should explicitly decide which they want. Both are useful at different times; both are possible with
forloops; but the former is already possible with simple function calls, and the latter is yet not.for await (const v of input) f(v);for (const v of await Promise.all(input)) f(v);await Array.fromAsync(input)await Promise.all(Array.from(input))Hopefully this choice is understandable.
I plan to close this issue when I update the explainer to mention this issue.
It sounds like
Array.from(await Promise.all(myPromiseArray))is what you’re looking for. There is no reason an unyielded value should magically be handled on your behalf.We absolutely must not do that -
Array.fromtakes an iterable, or failing that, an arraylike - it never checks or cares if something’s an actual Array. Similarly,Array.fromAsyncnot check or care - it takes an async iterable, or failing that, an iterable, or failing that, an arraylike.To clarify,
fromAsynctakes an iterable, it doesn’t take an array. That’s where the distinction is. There is no way to know from just holding an iterable whether it has eagerly created all its values or not, and the safe thing to do is assume it hasn’t.I also doubt we’d want to brand check the argument to see if the iterable is an array or a generic iterable. Now it’s possible a generic utility would be useful to turn an array of promises into an async iterable that handles rejections in the way you expect them. For example it’d attach a
.catchto all promises eagerly and recreate a new promise for caught unyielded promised on iterator close.Right, if you generate the promise when consumed by the iterator, it behaves as expected:
The Async-from-Sync Iterator objects do not eagerly consume the sync iterator. The problem in the original example was that it eagerly created the promises, which included an unhandled rejection.
I’m a bit confused. You made a rejected promise and didn’t handle it. I’d expect an unhandled rejection here, just like in for-of.
I think I see what you mean now. Because
delay(2000, true)rejects beforeArray.fromAsynccan Await it,Array.fromAsyncis unable to add a rejection handler todelay(2000, true)before it does reject.However, as you know, Awaiting an already-rejected promise will in turn throw an error in the current async context. So my understanding is that Array.fromAsync should still return a promise that will reject at step 3.j.iii once it does reach
delay(2000, true), even ifdelay(2000, true)already has rejected.Anyways, I will raise this with the proposal’s reviewers (CC: @ljharb, @nicolo-ribaudo) and see if this is truly a problem and if we need to do anything special about this. I would wish for no unhandled rejection to ever escape this function, but there probably is no way to avoid that with
for awaitsequential-read semantics.Perhaps it is then a matter of whether those input promises count as being “inside” Array.fromAsync (and handled by that function) rather than “outside” Array.fromAsync; if they don’t count as being “inside”, then no rejection can escape Array.fromAsync, because those input promises were never inside Array.fromAsync in the first place.
I suppose we could adopt
Promise.allsemantics for sync iterables and synchronously dump sync iterables before immediately awaiting all of them in parallel…but that feels pretty strange to me. My intuition is to matchfor awaitand sequentially read values, to match semantics with async iterables.