Summary
Promise.all, Promise.race, Promise.allSettled, and Promise.any should accept any iterable and reject with a TypeError when the argument is not iterable. Perry currently lowers these combinators as array-only calls by unboxing the first argument as an ArrayHeader*; missing arguments return a numeric stub instead of a rejected promise.
Expected Node behavior
Local Node v25.9.0 probe:
await Promise.all(new Set([Promise.resolve(1), 2]));
// [1, 2]
await Promise.allSettled(new Set([Promise.resolve(1), Promise.reject("x")]));
// [{ status: "fulfilled", value: 1 }, { status: "rejected", reason: "x" }]
await Promise.race(new Set([Promise.resolve("a")]));
// "a"
await Promise.any(new Set([Promise.reject("x"), Promise.resolve("b")]));
// "b"
await Promise.all(1);
// rejects TypeError: number 1 is not iterable
await Promise.any([]).catch(e => [e.name, e.errors.length]);
// ["AggregateError", 0]
The same iterable/non-iterable contract applies when the argument is omitted, because undefined is not iterable.
Current Perry implementation
Source evidence from current tree:
crates/perry-codegen/src/lower_call/console_promise.rs handles Promise.all / race / allSettled / any in one branch by lowering only args[0], unboxing it with unbox_to_i64, and calling js_promise_all, js_promise_race, js_promise_all_settled, or js_promise_any with that one pointer.
- The same lowering branch returns
double_literal(0.0) when args.is_empty(), so Promise.all() / Promise.race() / Promise.allSettled() / Promise.any() do not return the rejected Promise Node exposes for undefined input.
crates/perry-runtime/src/promise/combinators.rs::js_promise_all() documents and accepts promises_arr: *const ArrayHeader; js_promise_race, js_promise_all_settled, and js_promise_any have the same array-pointer shape.
- These helpers treat a null array pointer as an empty input path:
all / allSettled resolve with [], race returns pending, and any builds the empty rejection path. Node should reject non-iterable inputs with TypeError, not silently reinterpret them as empty arrays.
Suggested test surface
Add parity tests for iterable inputs and invalid inputs:
assert.deepEqual(await Promise.all(new Set([Promise.resolve(1), 2])), [1, 2]);
const settled = await Promise.allSettled(new Set([Promise.resolve(1), Promise.reject("x")]));
assert.deepEqual(settled.map(r => r.status), ["fulfilled", "rejected"]);
assert.equal(await Promise.race(new Set([Promise.resolve("a")])), "a");
assert.equal(await Promise.any(new Set([Promise.reject("x"), Promise.resolve("b")])), "b");
await assert.rejects(() => Promise.all(1 as any), TypeError);
await assert.rejects(() => Promise.all(undefined as any), TypeError);
await assert.rejects(() => Promise.allSettled(1 as any), TypeError);
await assert.rejects(() => Promise.race(1 as any), TypeError);
await assert.rejects(() => Promise.any(1 as any), TypeError);
Keep the existing Promise.any([]) AggregateError behavior covered as a control.
Scope / non-goals
This is not the closed missing-feature work from #266 / #268; the combinator helpers now exist. This issue tracks the remaining input contract: iterable consumption, omitted/non-iterable rejection, and avoiding array-pointer reinterpretation for non-array receivers.
Summary
Promise.all,Promise.race,Promise.allSettled, andPromise.anyshould accept any iterable and reject with aTypeErrorwhen the argument is not iterable. Perry currently lowers these combinators as array-only calls by unboxing the first argument as anArrayHeader*; missing arguments return a numeric stub instead of a rejected promise.Expected Node behavior
Local Node v25.9.0 probe:
The same iterable/non-iterable contract applies when the argument is omitted, because
undefinedis not iterable.Current Perry implementation
Source evidence from current tree:
crates/perry-codegen/src/lower_call/console_promise.rshandlesPromise.all/race/allSettled/anyin one branch by lowering onlyargs[0], unboxing it withunbox_to_i64, and callingjs_promise_all,js_promise_race,js_promise_all_settled, orjs_promise_anywith that one pointer.double_literal(0.0)whenargs.is_empty(), soPromise.all()/Promise.race()/Promise.allSettled()/Promise.any()do not return the rejected Promise Node exposes forundefinedinput.crates/perry-runtime/src/promise/combinators.rs::js_promise_all()documents and acceptspromises_arr: *const ArrayHeader;js_promise_race,js_promise_all_settled, andjs_promise_anyhave the same array-pointer shape.all/allSettledresolve with[],racereturns pending, andanybuilds the empty rejection path. Node should reject non-iterable inputs withTypeError, not silently reinterpret them as empty arrays.Suggested test surface
Add parity tests for iterable inputs and invalid inputs:
Keep the existing
Promise.any([])AggregateError behavior covered as a control.Scope / non-goals
This is not the closed missing-feature work from #266 / #268; the combinator helpers now exist. This issue tracks the remaining input contract: iterable consumption, omitted/non-iterable rejection, and avoiding array-pointer reinterpretation for non-array receivers.