Skip to content

Inconsistent import() promise order for TLA rejections vs fulfillments #3589

Open
@nicolo-ribaudo

Description

@nicolo-ribaudo

Consider the test case below.

There are two modules (a and b), with a depending on b. b uses top-level await. We dynamically import a and b, and then settle the promise that b is waiting on.

// entrypoint.js

import { p1, pA, pB } from "./setup.js";

let logs = [];

const importsP = Promise.all([
  import("./a.js").finally(() => logs.push(1)).catch(() => {}),
  import("./b.js").finally(() => logs.push(2)).catch(() => {}),
])

// Wait for evaluation of both graphs with entyrpoints in A and B to start before
// settling the promise that B is blocked on.
Promise.all([pA.promise, pB.promise]).then(p1.resolve);

importsP.then(() => {
  print(logs);
});
// setup.js
export const p1 = Promise.withResolvers();
export const pA = Promise.withResolvers();
export const pB = Promise.withResolvers();
// a.js
import "./a-sentinel.js";
import "./b.js";
// b.js
import "./b-sentinel.js";
import { p1 } from "./setup.js";
await p1.promise;
// a-sentinel.js
import { pA } from "./setup.js";
pA.resolve();
// b-sentinel.js
import { pB } from "./setup.js";
pB.resolve();
  • If we fullfill p1 (p1.resolve()), then the logs order will be 2,1 (b's promise settles first). This is because AsyncModuleExecutionFulfilled(module) (where module is b) first resolves module.[[TopLevelCapability]] and then recurses to its parents.
  • If we reject p1 (p1.reject()), then the logs order will be 1,2 (a's promise settles first). This is because AsyncModuleExecutionRejected(module) first recurses to module's parents and then rejects module.[[TopLevelCapability]].

Obviously the order for the .resolve() case cannot change (because a could itself use TLA), but the order for .reject() can be changed because there is no code execution between when b errors and all its ancestors get rejected.

My eshost is broken, but I tried in Deno and Bun and neither follows the spec:

  • Deno logs 2,1 in both cases
  • Bun logs 1,2 in both cases

I propose that we change the spec to match what Deno/V8 is doing.

This is not covered by test262, so even if we decide to keep the current behavior we need to add a test.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions