From 32086c7baeba54ac796e9d77149d78f2e9a77d6f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 17:30:18 +0000 Subject: [PATCH 1/2] Reject a module's own capability before its async parents AsyncModuleExecutionRejected rejects the module's own [[TopLevelCapability]] first and only then recurses into [[AsyncParentModules]], so the top-level promises of a failing graph settle leaf-to-root: 9. If module.[[TopLevelCapability]] is not empty, then b. Perform ! Call(module.[[TopLevelCapability]].[[Reject]], ...). 10. For each Cyclic Module Record m of module.[[AsyncParentModules]], do a. Perform AsyncModuleExecutionRejected(m, error). js_async_module_execution_rejected() runs those two steps the other way round, so an importer of the failing module is rejected before the module itself. Swap them. Fixes test262 language/module-code/top-level-await/rejection-order.js, which is listed in test262_errors.txt; drop the entry. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn --- quickjs.c | 16 ++++++++-------- test262_errors.txt | 1 - 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/quickjs.c b/quickjs.c index bbac00c33..ca047acb0 100644 --- a/quickjs.c +++ b/quickjs.c @@ -32138,14 +32138,6 @@ static JSValue js_async_module_execution_rejected(JSContext *ctx, JSValueConst t module->eval_exception = js_dup(error); module->status = JS_MODULE_STATUS_EVALUATED; - for(i = 0; i < module->async_parent_modules_count; i++) { - JSModuleDef *m = module->async_parent_modules[i]; - JSValue m_obj = JS_NewModuleValue(ctx, m); - js_async_module_execution_rejected(ctx, JS_UNDEFINED, 1, &error, 0, - vc(&m_obj)); - JS_FreeValue(ctx, m_obj); - } - if (!JS_IsUndefined(module->promise)) { JSValue ret_val; assert(module->cycle_root == module); @@ -32153,6 +32145,14 @@ static JSValue js_async_module_execution_rejected(JSContext *ctx, JSValueConst t 1, &error); JS_FreeValue(ctx, ret_val); } + + for(i = 0; i < module->async_parent_modules_count; i++) { + JSModuleDef *m = module->async_parent_modules[i]; + JSValue m_obj = JS_NewModuleValue(ctx, m); + js_async_module_execution_rejected(ctx, JS_UNDEFINED, 1, &error, 0, + vc(&m_obj)); + JS_FreeValue(ctx, m_obj); + } return JS_UNDEFINED; } diff --git a/test262_errors.txt b/test262_errors.txt index 082fb016a..1c2cefbae 100644 --- a/test262_errors.txt +++ b/test262_errors.txt @@ -36,7 +36,6 @@ test262/test/language/module-code/ambiguous-export-bindings/namespace-unambiguou test262/test/language/module-code/ambiguous-export-bindings/namespace-unambiguous-if-export-star-as-from.js:75: SyntaxError: export 'foo' in module 'test262/test/language/module-code/ambiguous-export-bindings/nam' is ambiguous test262/test/language/module-code/ambiguous-export-bindings/namespace-unambiguous-if-import-star-as-and-export.js:74: SyntaxError: export 'foo' in module 'test262/test/language/module-code/ambiguous-export-bindings/nam' is ambiguous test262/test/language/module-code/top-level-await/module-graphs-does-not-hang.js:10: TypeError: $DONE() not called -test262/test/language/module-code/top-level-await/rejection-order.js:20: TypeError: $DONE() not called test262/test/language/statements/await-using/initializer-Symbol.asyncDispose-disposed-at-end-of-imported-module.js:11: SyntaxError: exported variable 'resource' does not exist test262/test/language/statements/await-using/initializer-Symbol.dispose-disposed-at-end-of-imported-module.js:11: SyntaxError: exported variable 'resource' does not exist test262/test/language/statements/class/elements/syntax/valid/grammar-field-named-get-followed-by-generator-asi.js:40: SyntaxError: invalid property name From bf116230fe5ba96b949ef2b5ca760d648cf17205 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 08:46:13 +0000 Subject: [PATCH 2/2] Add a test for the async module rejection order A three module chain, each link imported in its own right so that each one has a top level capability, makes the order observable: rejecting the module's own capability before recursing into its async parents settles the chain leaf first, and doing it the other way around settles it in reverse. Also checks that every module in the chain reports the identical error object, that re-importing a failed module keeps reporting it, and that two independent ancestors of one failing leaf are both notified. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01K6eRbuuuCujKgQkrHgvMrc --- tests.conf | 6 +++ tests/fixture_reject_leaf.js | 3 ++ tests/fixture_reject_mid.js | 2 + tests/fixture_reject_sib_a.js | 2 + tests/fixture_reject_sib_b.js | 2 + tests/fixture_reject_sib_leaf.js | 2 + tests/fixture_reject_top.js | 2 + tests/module-async-rejection-order.js | 61 +++++++++++++++++++++++++++ 8 files changed, 80 insertions(+) create mode 100644 tests/fixture_reject_leaf.js create mode 100644 tests/fixture_reject_mid.js create mode 100644 tests/fixture_reject_sib_a.js create mode 100644 tests/fixture_reject_sib_b.js create mode 100644 tests/fixture_reject_sib_leaf.js create mode 100644 tests/fixture_reject_top.js create mode 100644 tests/module-async-rejection-order.js diff --git a/tests.conf b/tests.conf index 192656e0b..e5d17b07c 100644 --- a/tests.conf +++ b/tests.conf @@ -16,3 +16,9 @@ tests/fixture_throwing_module.js tests/fixture_reexport_source.js tests/fixture_reexport_missing.js tests/fixture_reexport_direct.js +tests/fixture_reject_leaf.js +tests/fixture_reject_mid.js +tests/fixture_reject_top.js +tests/fixture_reject_sib_leaf.js +tests/fixture_reject_sib_a.js +tests/fixture_reject_sib_b.js diff --git a/tests/fixture_reject_leaf.js b/tests/fixture_reject_leaf.js new file mode 100644 index 000000000..a6df1b2db --- /dev/null +++ b/tests/fixture_reject_leaf.js @@ -0,0 +1,3 @@ +/* the async module that actually fails */ +await Promise.resolve(); +throw new Error("leaf rejected"); diff --git a/tests/fixture_reject_mid.js b/tests/fixture_reject_mid.js new file mode 100644 index 000000000..413867076 --- /dev/null +++ b/tests/fixture_reject_mid.js @@ -0,0 +1,2 @@ +import "./fixture_reject_leaf.js"; +export const mid = 1; diff --git a/tests/fixture_reject_sib_a.js b/tests/fixture_reject_sib_a.js new file mode 100644 index 000000000..f80a641a0 --- /dev/null +++ b/tests/fixture_reject_sib_a.js @@ -0,0 +1,2 @@ +import "./fixture_reject_sib_leaf.js"; +export const a = 1; diff --git a/tests/fixture_reject_sib_b.js b/tests/fixture_reject_sib_b.js new file mode 100644 index 000000000..c5a49bef2 --- /dev/null +++ b/tests/fixture_reject_sib_b.js @@ -0,0 +1,2 @@ +import "./fixture_reject_sib_leaf.js"; +export const b = 1; diff --git a/tests/fixture_reject_sib_leaf.js b/tests/fixture_reject_sib_leaf.js new file mode 100644 index 000000000..c2d043d74 --- /dev/null +++ b/tests/fixture_reject_sib_leaf.js @@ -0,0 +1,2 @@ +await Promise.resolve(); +throw new Error("sibling leaf rejected"); diff --git a/tests/fixture_reject_top.js b/tests/fixture_reject_top.js new file mode 100644 index 000000000..57306af56 --- /dev/null +++ b/tests/fixture_reject_top.js @@ -0,0 +1,2 @@ +import "./fixture_reject_mid.js"; +export const top = 1; diff --git a/tests/module-async-rejection-order.js b/tests/module-async-rejection-order.js new file mode 100644 index 000000000..d6dd50229 --- /dev/null +++ b/tests/module-async-rejection-order.js @@ -0,0 +1,61 @@ +import { assert } from "./assert.js"; + +/* AsyncModuleExecutionRejected rejects the module's own [[TopLevelCapability]] + before recursing into [[AsyncParentModules]], so a failing async module + settles leaf first and root last. Doing it the other way around settles the + whole chain in reverse. */ + +/* Importing the leaf first, then each ancestor in turn, gives every module in + the chain its own top level capability, which is what makes the order + observable at all. */ +const order = []; +const errors = []; + +function record(name) { + return e => { order.push(name); errors.push(e); }; +} + +const leaf = import("./fixture_reject_leaf.js").then(record("leaf.ok"), + record("leaf")); +const mid = import("./fixture_reject_mid.js").then(record("mid.ok"), + record("mid")); +const top = import("./fixture_reject_top.js").then(record("top.ok"), + record("top")); + +await Promise.all([leaf, mid, top]); + +assert(order.join(" -> "), "leaf -> mid -> top"); + +/* every module in the chain reports the leaf's error, and it is the very + same object, not a copy */ +assert(errors.length, 3); +for (const e of errors) { + assert(e instanceof Error, true); + assert(e.message, "leaf rejected"); + assert(e === errors[0], true); +} + +/* re-importing the failed modules keeps reporting the same error and does + not re-run anything */ +{ + const again = []; + await Promise.all([ + import("./fixture_reject_leaf.js").then(() => again.push("ok"), + e => again.push(e.message)), + import("./fixture_reject_top.js").then(() => again.push("ok"), + e => again.push(e.message)), + ]); + assert(again.length, 2); + assert(again[0], "leaf rejected"); + assert(again[1], "leaf rejected"); +} + +/* two independent ancestors of the same failing leaf are both notified */ +{ + const seen = []; + const a = import("./fixture_reject_sib_a.js").catch(e => seen.push("a")); + const b = import("./fixture_reject_sib_b.js").catch(e => seen.push("b")); + await Promise.all([a, b]); + seen.sort(); + assert(seen.join(","), "a,b"); +}