From 912c2482511789b9b70c98e406ce750eb3d156a2 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Fri, 24 Jul 2026 17:22:01 +0530 Subject: [PATCH] Fix Array.fromAsync iterator close semantics Array.fromAsync closed its async iterator in a finally block, which calls return() even when the iterator completed normally, and does not await the result. The spec only closes the iterator on an abrupt completion (IfAbruptCloseAsyncIterator, from the element await or the mapper), leaves an exhausted iterator alone, and awaits the return() result before the returned promise settles. Close the iterator from a per-element catch instead: await return() on failure, rethrow the original error even if return() itself throws, and skip the close entirely on normal completion. --- builtin-array-fromasync.h | 157 ++++++++++++------------ builtin-array-fromasync.js | 15 ++- tests/array-fromasync-iterator-close.js | 104 ++++++++++++++++ 3 files changed, 193 insertions(+), 83 deletions(-) create mode 100644 tests/array-fromasync-iterator-close.js diff --git a/builtin-array-fromasync.h b/builtin-array-fromasync.h index 4a53f380c..7c6f51e00 100644 --- a/builtin-array-fromasync.h +++ b/builtin-array-fromasync.h @@ -2,10 +2,10 @@ #include -const uint32_t qjsc_builtin_array_fromasync_size = 888; +const uint32_t qjsc_builtin_array_fromasync_size = 909; -const uint8_t qjsc_builtin_array_fromasync[888] = { - 0x1b, 0xc5, 0x39, 0x53, 0x3c, 0x0e, 0x01, 0x28, +const uint8_t qjsc_builtin_array_fromasync[909] = { + 0x1b, 0x77, 0xbb, 0x7e, 0x5e, 0x0e, 0x01, 0x28, 0x53, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0xb7, 0x61, 0x73, 0x79, 0x6e, 0x63, 0x49, 0x74, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x01, 0x2a, 0x4f, 0x62, @@ -34,8 +34,8 @@ const uint8_t qjsc_builtin_array_fromasync[888] = { 0x00, 0xe4, 0x03, 0x00, 0x01, 0x40, 0x01, 0xe6, 0x03, 0x00, 0x01, 0x40, 0x04, 0xe8, 0x03, 0x00, 0x01, 0x40, 0x02, 0x0c, 0x60, 0x02, 0x01, 0x88, - 0x02, 0x03, 0x0e, 0x01, 0x06, 0x00, 0x05, 0x00, - 0x93, 0x04, 0x11, 0xea, 0x03, 0x00, 0x01, 0x00, + 0x02, 0x03, 0x0f, 0x01, 0x06, 0x00, 0x05, 0x00, + 0xa4, 0x04, 0x12, 0xea, 0x03, 0x00, 0x01, 0x00, 0xec, 0x03, 0x00, 0x01, 0x00, 0xee, 0x03, 0x00, 0x01, 0x00, 0xea, 0x03, 0x01, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x20, 0xec, 0x03, 0x01, 0x01, 0x20, @@ -44,77 +44,80 @@ const uint8_t qjsc_builtin_array_fromasync[888] = { 0x03, 0x02, 0x05, 0x20, 0xf6, 0x03, 0x02, 0x06, 0x20, 0xf8, 0x03, 0x02, 0x07, 0x20, 0x66, 0x06, 0x08, 0x20, 0x88, 0x01, 0x07, 0x09, 0x20, 0xfa, - 0x03, 0x0a, 0x08, 0x30, 0x88, 0x01, 0x0d, 0x0b, - 0x20, 0xe2, 0x01, 0x0d, 0x0c, 0x20, 0x10, 0x00, - 0x01, 0x00, 0xb4, 0x03, 0x01, 0x01, 0xe4, 0x03, - 0x02, 0x01, 0xe8, 0x03, 0x04, 0x01, 0xbe, 0x02, - 0x00, 0x01, 0xe6, 0x03, 0x03, 0x01, 0x08, 0xc8, - 0x0d, 0x60, 0x02, 0x00, 0x60, 0x01, 0x00, 0x60, - 0x00, 0x00, 0xd7, 0xcf, 0xd8, 0x11, 0xf8, 0xf0, - 0x08, 0x0e, 0x38, 0x49, 0x00, 0x00, 0x00, 0xe0, - 0xd0, 0xd9, 0x11, 0xf8, 0xf0, 0x08, 0x0e, 0x38, - 0x49, 0x00, 0x00, 0x00, 0xe1, 0xd1, 0x60, 0x07, - 0x00, 0x60, 0x06, 0x00, 0x60, 0x05, 0x00, 0x60, - 0x04, 0x00, 0x60, 0x03, 0x00, 0xd8, 0x38, 0x49, - 0x00, 0x00, 0x00, 0xae, 0xf0, 0x16, 0xd8, 0x96, - 0x04, 0x1b, 0x00, 0x00, 0x00, 0xae, 0xf0, 0x0c, - 0xe3, 0x11, 0x04, 0xfe, 0x00, 0x00, 0x00, 0x21, - 0x01, 0x00, 0x30, 0x06, 0xd2, 0xba, 0xc8, 0x04, - 0xc7, 0x0d, 0xfb, 0xc8, 0x05, 0x09, 0xc8, 0x06, - 0xd7, 0xe4, 0x46, 0xc8, 0x07, 0x61, 0x07, 0x00, - 0x07, 0xab, 0xf0, 0x0f, 0x0a, 0x11, 0x62, 0x06, - 0x00, 0x0e, 0xd7, 0xe5, 0x46, 0x11, 0x62, 0x07, - 0x00, 0x0e, 0x61, 0x07, 0x00, 0x07, 0xab, 0x68, - 0xac, 0x00, 0x00, 0x00, 0x60, 0x08, 0x00, 0x06, - 0x11, 0xf8, 0xf1, 0x0c, 0x70, 0x41, 0x33, 0x00, - 0x00, 0x00, 0xc8, 0x08, 0x0e, 0xf2, 0x05, 0x0e, - 0xd7, 0xf2, 0xf2, 0x61, 0x08, 0x00, 0x8c, 0x11, - 0xf1, 0x03, 0x0e, 0xba, 0x11, 0x62, 0x08, 0x00, - 0x0e, 0x61, 0x05, 0x00, 0xf0, 0x0c, 0xc7, 0x0d, - 0x11, 0x61, 0x08, 0x00, 0x21, 0x01, 0x00, 0xf2, - 0x06, 0xe6, 0x61, 0x08, 0x00, 0xf5, 0x11, 0x62, - 0x03, 0x00, 0x0e, 0x61, 0x04, 0x00, 0x61, 0x08, - 0x00, 0xa5, 0x68, 0x37, 0x01, 0x00, 0x00, 0x60, - 0x09, 0x00, 0xd7, 0x61, 0x04, 0x00, 0x46, 0xc8, - 0x09, 0x61, 0x06, 0x00, 0xf0, 0x0a, 0x61, 0x09, - 0x00, 0x8a, 0x11, 0x62, 0x09, 0x00, 0x0e, 0xd8, - 0xf0, 0x17, 0xd8, 0x41, 0xff, 0x00, 0x00, 0x00, - 0xd9, 0x61, 0x09, 0x00, 0x61, 0x04, 0x00, 0x24, - 0x03, 0x00, 0x8a, 0x11, 0x62, 0x09, 0x00, 0x0e, - 0x5d, 0x04, 0x00, 0x61, 0x03, 0x00, 0x61, 0x04, - 0x00, 0x90, 0x62, 0x04, 0x00, 0x0b, 0x61, 0x09, - 0x00, 0x4b, 0x44, 0x00, 0x00, 0x00, 0x0a, 0x4b, - 0x41, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x42, 0x00, - 0x00, 0x00, 0x0a, 0x4b, 0x43, 0x00, 0x00, 0x00, - 0xf7, 0x0e, 0xf2, 0x98, 0x60, 0x0a, 0x00, 0x61, - 0x07, 0x00, 0x41, 0xff, 0x00, 0x00, 0x00, 0xd7, - 0x24, 0x01, 0x00, 0xc8, 0x0a, 0x61, 0x05, 0x00, - 0xf0, 0x09, 0xc7, 0x0d, 0x11, 0x21, 0x00, 0x00, - 0xf2, 0x03, 0xe6, 0xf4, 0x11, 0x62, 0x03, 0x00, - 0x0e, 0x6b, 0x93, 0x00, 0x00, 0x00, 0x60, 0x0c, - 0x00, 0x60, 0x0b, 0x00, 0x06, 0x11, 0xf8, 0xf1, - 0x13, 0x70, 0x41, 0x44, 0x00, 0x00, 0x00, 0xc8, - 0x0b, 0x41, 0x71, 0x00, 0x00, 0x00, 0xc8, 0x0c, - 0x0e, 0xf2, 0x10, 0x0e, 0x61, 0x0a, 0x00, 0x41, - 0x72, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x8a, - 0xf2, 0xe0, 0x61, 0x0c, 0x00, 0xf1, 0x55, 0x61, - 0x06, 0x00, 0xf0, 0x0a, 0x61, 0x0b, 0x00, 0x8a, - 0x11, 0x62, 0x0b, 0x00, 0x0e, 0xd8, 0xf0, 0x17, - 0xd8, 0x41, 0xff, 0x00, 0x00, 0x00, 0xd9, 0x61, - 0x0b, 0x00, 0x61, 0x04, 0x00, 0x24, 0x03, 0x00, - 0x8a, 0x11, 0x62, 0x0b, 0x00, 0x0e, 0x5d, 0x04, - 0x00, 0x61, 0x03, 0x00, 0x61, 0x04, 0x00, 0x90, - 0x62, 0x04, 0x00, 0x0b, 0x61, 0x0b, 0x00, 0x4b, - 0x44, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x41, 0x00, - 0x00, 0x00, 0x0a, 0x4b, 0x42, 0x00, 0x00, 0x00, - 0x0a, 0x4b, 0x43, 0x00, 0x00, 0x00, 0xf7, 0x0e, - 0xf3, 0x7d, 0xff, 0x0e, 0x06, 0x6c, 0x0d, 0x00, - 0x00, 0x00, 0x0e, 0xf2, 0x1e, 0x6c, 0x05, 0x00, - 0x00, 0x00, 0x30, 0x61, 0x0a, 0x00, 0x40, 0x06, - 0x00, 0x00, 0x00, 0xf0, 0x0d, 0x61, 0x0a, 0x00, - 0x41, 0x06, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, - 0x0e, 0x6d, 0x61, 0x03, 0x00, 0x61, 0x04, 0x00, - 0x42, 0x33, 0x00, 0x00, 0x00, 0x61, 0x03, 0x00, - 0x2f, 0xc5, 0x00, 0x28, 0xc5, 0x00, 0xd3, 0x28, + 0x03, 0x0a, 0x08, 0x30, 0x88, 0x01, 0x0c, 0x0b, + 0x20, 0xe2, 0x01, 0x0c, 0x0c, 0x20, 0x6e, 0x11, + 0x0d, 0x03, 0x10, 0x00, 0x01, 0x00, 0xb4, 0x03, + 0x01, 0x01, 0xe4, 0x03, 0x02, 0x01, 0xe8, 0x03, + 0x04, 0x01, 0xbe, 0x02, 0x00, 0x01, 0xe6, 0x03, + 0x03, 0x01, 0x08, 0xc8, 0x0e, 0x60, 0x02, 0x00, + 0x60, 0x01, 0x00, 0x60, 0x00, 0x00, 0xd7, 0xcf, + 0xd8, 0x11, 0xf8, 0xf0, 0x08, 0x0e, 0x38, 0x49, + 0x00, 0x00, 0x00, 0xe0, 0xd0, 0xd9, 0x11, 0xf8, + 0xf0, 0x08, 0x0e, 0x38, 0x49, 0x00, 0x00, 0x00, + 0xe1, 0xd1, 0x60, 0x07, 0x00, 0x60, 0x06, 0x00, + 0x60, 0x05, 0x00, 0x60, 0x04, 0x00, 0x60, 0x03, + 0x00, 0xd8, 0x38, 0x49, 0x00, 0x00, 0x00, 0xae, + 0xf0, 0x16, 0xd8, 0x96, 0x04, 0x1b, 0x00, 0x00, + 0x00, 0xae, 0xf0, 0x0c, 0xe3, 0x11, 0x04, 0xfe, + 0x00, 0x00, 0x00, 0x21, 0x01, 0x00, 0x30, 0x06, + 0xd2, 0xba, 0xc8, 0x04, 0xc7, 0x0e, 0xfb, 0xc8, + 0x05, 0x09, 0xc8, 0x06, 0xd7, 0xe4, 0x46, 0xc8, + 0x07, 0x61, 0x07, 0x00, 0x07, 0xab, 0xf0, 0x0f, + 0x0a, 0x11, 0x62, 0x06, 0x00, 0x0e, 0xd7, 0xe5, + 0x46, 0x11, 0x62, 0x07, 0x00, 0x0e, 0x61, 0x07, + 0x00, 0x07, 0xab, 0x68, 0xac, 0x00, 0x00, 0x00, + 0x60, 0x08, 0x00, 0x06, 0x11, 0xf8, 0xf1, 0x0c, + 0x70, 0x41, 0x33, 0x00, 0x00, 0x00, 0xc8, 0x08, + 0x0e, 0xf2, 0x05, 0x0e, 0xd7, 0xf2, 0xf2, 0x61, + 0x08, 0x00, 0x8c, 0x11, 0xf1, 0x03, 0x0e, 0xba, + 0x11, 0x62, 0x08, 0x00, 0x0e, 0x61, 0x05, 0x00, + 0xf0, 0x0c, 0xc7, 0x0e, 0x11, 0x61, 0x08, 0x00, + 0x21, 0x01, 0x00, 0xf2, 0x06, 0xe6, 0x61, 0x08, + 0x00, 0xf5, 0x11, 0x62, 0x03, 0x00, 0x0e, 0x61, + 0x04, 0x00, 0x61, 0x08, 0x00, 0xa5, 0x68, 0x48, + 0x01, 0x00, 0x00, 0x60, 0x09, 0x00, 0xd7, 0x61, + 0x04, 0x00, 0x46, 0xc8, 0x09, 0x61, 0x06, 0x00, + 0xf0, 0x0a, 0x61, 0x09, 0x00, 0x8a, 0x11, 0x62, + 0x09, 0x00, 0x0e, 0xd8, 0xf0, 0x17, 0xd8, 0x41, + 0xff, 0x00, 0x00, 0x00, 0xd9, 0x61, 0x09, 0x00, + 0x61, 0x04, 0x00, 0x24, 0x03, 0x00, 0x8a, 0x11, + 0x62, 0x09, 0x00, 0x0e, 0x5d, 0x04, 0x00, 0x61, + 0x03, 0x00, 0x61, 0x04, 0x00, 0x90, 0x62, 0x04, + 0x00, 0x0b, 0x61, 0x09, 0x00, 0x4b, 0x44, 0x00, + 0x00, 0x00, 0x0a, 0x4b, 0x41, 0x00, 0x00, 0x00, + 0x0a, 0x4b, 0x42, 0x00, 0x00, 0x00, 0x0a, 0x4b, + 0x43, 0x00, 0x00, 0x00, 0xf7, 0x0e, 0xf2, 0x98, + 0x60, 0x0a, 0x00, 0x61, 0x07, 0x00, 0x41, 0xff, + 0x00, 0x00, 0x00, 0xd7, 0x24, 0x01, 0x00, 0xc8, + 0x0a, 0x61, 0x05, 0x00, 0xf0, 0x09, 0xc7, 0x0e, + 0x11, 0x21, 0x00, 0x00, 0xf2, 0x03, 0xe6, 0xf4, + 0x11, 0x62, 0x03, 0x00, 0x0e, 0x60, 0x0c, 0x00, + 0x60, 0x0b, 0x00, 0x06, 0x11, 0xf8, 0xf1, 0x13, + 0x70, 0x41, 0x44, 0x00, 0x00, 0x00, 0xc8, 0x0b, + 0x41, 0x71, 0x00, 0x00, 0x00, 0xc8, 0x0c, 0x0e, + 0xf2, 0x10, 0x0e, 0x61, 0x0a, 0x00, 0x41, 0x72, + 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x8a, 0xf2, + 0xe0, 0x61, 0x0c, 0x00, 0x69, 0x92, 0x00, 0x00, + 0x00, 0x6b, 0x59, 0x00, 0x00, 0x00, 0x61, 0x06, + 0x00, 0xf0, 0x0a, 0x61, 0x0b, 0x00, 0x8a, 0x11, + 0x62, 0x0b, 0x00, 0x0e, 0xd8, 0xf0, 0x17, 0xd8, + 0x41, 0xff, 0x00, 0x00, 0x00, 0xd9, 0x61, 0x0b, + 0x00, 0x61, 0x04, 0x00, 0x24, 0x03, 0x00, 0x8a, + 0x11, 0x62, 0x0b, 0x00, 0x0e, 0x5d, 0x04, 0x00, + 0x61, 0x03, 0x00, 0x61, 0x04, 0x00, 0x90, 0x62, + 0x04, 0x00, 0x0b, 0x61, 0x0b, 0x00, 0x4b, 0x44, + 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x41, 0x00, 0x00, + 0x00, 0x0a, 0x4b, 0x42, 0x00, 0x00, 0x00, 0x0a, + 0x4b, 0x43, 0x00, 0x00, 0x00, 0xf7, 0x0e, 0x0e, + 0xf3, 0x74, 0xff, 0xc8, 0x0d, 0x6b, 0x30, 0x00, + 0x00, 0x00, 0x6b, 0x1e, 0x00, 0x00, 0x00, 0x61, + 0x0a, 0x00, 0x40, 0x06, 0x00, 0x00, 0x00, 0xf0, + 0x0e, 0x61, 0x0a, 0x00, 0x41, 0x06, 0x00, 0x00, + 0x00, 0x24, 0x00, 0x00, 0x8a, 0x0e, 0x0e, 0xf2, + 0x0b, 0x0e, 0x6b, 0x07, 0x00, 0x00, 0x00, 0x0e, + 0xf2, 0x02, 0x30, 0xc7, 0x0d, 0x30, 0x30, 0x61, + 0x03, 0x00, 0x61, 0x04, 0x00, 0x42, 0x33, 0x00, + 0x00, 0x00, 0x61, 0x03, 0x00, 0x2f, 0xc5, 0x00, + 0x28, 0xc5, 0x00, 0xd3, 0x28, }; diff --git a/builtin-array-fromasync.js b/builtin-array-fromasync.js index cfe09ee6b..03d17032c 100644 --- a/builtin-array-fromasync.js +++ b/builtin-array-fromasync.js @@ -18,16 +18,19 @@ } else { const iter = method.call(arrayLike) result = isConstructor ? new this() : Array() - try { - for (;;) { - let {value, done} = await iter.next() - if (done) break + for (;;) { + let {value, done} = await iter.next() + if (done) break + try { if (sync) value = await value if (mapFn) value = await mapFn.call(thisArg, value, i) Object·defineProperty(result, i++, {value, configurable: true, writable: true, enumerable: true}) + } catch (error) { + try { + if (iter.return) await iter.return() + } catch {} + throw error } - } finally { - if (iter.return) iter.return() } } result.length = i diff --git a/tests/array-fromasync-iterator-close.js b/tests/array-fromasync-iterator-close.js new file mode 100644 index 000000000..7e7bf4dcf --- /dev/null +++ b/tests/array-fromasync-iterator-close.js @@ -0,0 +1,104 @@ +import { assert } from "./assert.js"; + +/* Array.fromAsync iterator close semantics: + - a normal completion (done: true) must not call return() + - an abrupt completion from the mapper (or an element promise rejection) + must close the iterator, and the close must be awaited before the + Array.fromAsync promise settles */ + +/* return() is not called when the iterator is exhausted normally */ +{ + let returnCalled = false; + const iter = { + [Symbol.asyncIterator]() { + let i = 0; + return { + async next() { + return i < 2 ? { value: i++, done: false } + : { value: undefined, done: true }; + }, + async return() { + returnCalled = true; + return { done: true }; + }, + }; + }, + }; + await Array.fromAsync(iter); + assert(returnCalled, false); +} + +/* a throwing mapper closes the iterator */ +{ + let returnCalled = false; + const iter = { + [Symbol.asyncIterator]() { + return { + async next() { + return { value: 1, done: false }; + }, + async return() { + returnCalled = true; + return { done: true }; + }, + }; + }, + }; + let error; + try { + await Array.fromAsync(iter, () => { throw new Error("boom"); }); + } catch (e) { + error = e; + } + assert(error.message, "boom"); + assert(returnCalled, true); +} + +/* the close is awaited: return()'s promise settles before fromAsync's */ +{ + const order = []; + const iter = { + [Symbol.asyncIterator]() { + return { + async next() { + return { value: 1, done: false }; + }, + return() { + order.push("return"); + return new Promise((resolve) => { + Promise.resolve().then(() => { + order.push("closed"); + resolve({ done: true }); + }); + }); + }, + }; + }, + }; + await Array.fromAsync(iter, () => { throw new Error("boom"); }) + .catch(() => order.push("rejected")); + assert(order.join(","), "return,closed,rejected"); +} + +/* a rejected return() does not mask the original error */ +{ + const iter = { + [Symbol.asyncIterator]() { + return { + async next() { + return { value: 1, done: false }; + }, + async return() { + throw new Error("close failed"); + }, + }; + }, + }; + let error; + try { + await Array.fromAsync(iter, () => { throw new Error("original"); }); + } catch (e) { + error = e; + } + assert(error.message, "original"); +}