-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
What happens with rejections in synchronous iterables? #16
Comments
Good question. The answer should be yes, the input value’s rejection should be I believe that the Array.fromAsync specification reflects this desired behavior. Hopefully this answer is satisfactory. |
This is what makes me think it wouldn't handle the rejection. If its iterating through an asynchronous version of the iterator, doesn't it have to wait for the current next value to settle before moving on to the next next value? In the example array provided, while awaiting |
I think I see what you mean now. Because 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 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 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 |
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. |
Right, if you generate the promise when consumed by the iterator, it behaves as expected: const delay = (ms, reject = false) => new Promise((r, j) => setTimeout(reject ? j : r, ms, ms));
function * getIterator (config) {
for (const item of config) {
yield delay.apply(null, item);
}
}
for await (let ms of getIterator([[1000], [3000], [2000, true]])) {
console.log(ms) // 1000, 3000, (uncaught error 2000)
} 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. |
You can catch this in a normal for...of for (let promise of [delay(1000), delay(3000), delay(2000, true)]) {
promise.catch(() => 'handled')
} Promise.all will also handle it Promise.all([delay(1000), delay(3000), delay(2000, true)]) // rejects, but not unhandled for-await...of results in an unhandled because it fails to reach the rejection in time. So the question is, does fromAsync use Promise.all with sync iterables/array-likes (eager), or does it still for-await...of (lazy)? If an iterable is sync, is it meant to be iterated synchronously, even if it contains async (promise) values? Or is fromAsync going to wait and possibly allow for unhandled rejections? If the latter, I think the "supports sync" of this needs a big asterisk saying something to the effect of "you should probably be using Promise.all() instead" |
Right, that's because you eagerly (aka synchronously) consume all the promises produced.
A sync iterable means it synchronously produces values, it doesn't mean it eagerly creates them. Why should a consumer of the iterator assume it can eagerly consume these values? |
I would expect that if you did The awaiting done here is on the async iteration result, which contains the value but is not itself the value. |
To clarify, 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 async function * makeAsyncIteratorFromArray(arr) {
arr = arr.map((value) => Promise.resolve(value));
arr.forEach((promise) => promise.catch(() => {}));
let promise;
try {
while(promise = arr.shift()) {
yield await promise;
}
} finally {
while(promise = arr.shift()) {
new Promise((r) => r(promise));
}
}
} const delay = (ms, reject = false) => new Promise((r, j) => setTimeout(reject ? j : r, ms, ms));
for await (let ms of makeAsyncIteratorFromArray([delay(1000), delay(3000), delay(2000, true)])) {
console.log(ms)
if (ms === 3000) break;
}
// 1000, 3000, (unhandled rejection 2000) |
We absolutely must not do that - |
Wouldn't the safest thing to do be not try to assume either way? We can either eagerly consume sync iterators (and array-likes) or not. If we do, we save ourselves from possible unhandled rejections with eagerly created rejecting promises. If not we allow sync iterators to be consumed asynchronously which allows for lazy creation dependent on the results of prior iterations... though if they're implemented as sync iterators, would they even expect to be dependent on that in the first place? |
Unhandled rejections aren't something that needs "saving" from. If you don't want your rejections unhandled, you need to handle them. I don't see how |
I want to handle them but through the promise returned by
|
It sounds like |
But Promise.all is handling it. There's no magic there. It gets yielded, and Promise.all deals with it. Array.fromAsync could do that too. But should it? That's all I'm asking. Should it or shouldn't it? The way this conversation is going it seems like people are on the side of it shouldn't, that fromAsync is just a direct implementation of for-await...of without any special considerations for sync iterables. |
I suppose it could detect if a sync iterable is provided instead of an async iterable, and eagerly consume it. That would indeed be a different behavior than |
It definitely shouldn't - this proposal is not a Promise combinator, it's just condensing an async iterable to an array. |
Yeah, the counter argument to eager consumption is that a sync iterable that only generates activity on demand would now be forced to produce all its values. While there is a way too go from array of promise -> async iterator of value while handling rejections, there is no way to unconsume an eagerly consumed sync generator, which would also tilt me in the camp of not eagerly consuming a sync iterable by default. |
As I mentioned in #16 (comment), I agree with @ljharb and @mhofman. I think we should continue to use Async-from-Sync iterators (i.e., The current semantics are such that, if The expectation is that, as usual, the creator of the
Moreover, eager iteration then parallel awaiting is already possible with 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
Hopefully this choice is understandable. I plan to close this issue when I update the explainer to mention this issue. |
Closes #18. See also #16. Co-authored-by: Jordan Harband <[email protected]>
Closes #18. See also #16. Co-authored-by: Jordan Harband <[email protected]>
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 afor await
it goes unhandled. WouldArray.fromAsync()
be able to account for that and reject its returned promise or would it to allow for the unhandled rejection to slip through?The text was updated successfully, but these errors were encountered: