DEV Community

Leo
Leo

Posted on Originally published at news.html.to

Safari 27 rewrites the module loader to fix top-level await

I pulled the WebKit post open in one tab and the smallest possible module graph in another, because a rewrite of the loader is not the kind of note you skim. The short version: per WebKit, Safari 27 now runs top-level await on a fresh module loader that translates the ECMAScript specification's pseudocode directly into C++, replacing the old self-hosted JavaScript implementation. The longer version has some good archaeology in it.

What actually changed

Kai Tamkun's write-up on the WebKit blog, dated September 2, 2026, describes the switch plainly. "The old module loader was written in JavaScript as a self-hosted builtin." That loader traced back to a much older spec effort. Per the post, "The WebKit team wrote Safari's module loader long ago, back during the days of the WHATWG Loader proposal (last updated January 2016)." Top-level await, the ECMAScript 2022 feature that lets a module await at the top of its body, ended up sitting on top of that older shape. In Tamkun's words, "Top-level await was implemented in terms of a proposal that was abandoned before any support for async/await existed."

So the team stopped patching. They rewrote the loader, moving the algorithm from self-hosted JavaScript into C++ that follows the ECMAScript modules pseudocode step for step.

The bug I could actually reproduce

The demonstration in the post is a small one and worth reading whole. The imported module pauses at the top:

await new Promise(resolve => setTimeout(resolve, 10));

export function someFunction() {
    return "Hello!";
}
Enter fullscreen mode Exit fullscreen mode

Then the main script dynamically imports the same module three times and logs the order they complete in:

async function load(index) {
    try {
        print("Importing", index);
        const module = await import("./test-module.js");
        // ... rest of function
Enter fullscreen mode Exit fullscreen mode

Per the post, the old loader could finish those imports out of sequence — 2, 3, 1 rather than 1, 2, 3 — and code that read a top-level-awaited binding could throw Cannot access 'someArray' before initialization because, as Tamkun puts it, "the evaluation of the imported module hasn't completed yet." That is the flavour of intermittent failure that eats an afternoon.

Why they rebuilt instead of patched

The standing question for any spec-alignment work is patch or rewrite. WebKit chose rewrite. The reason given is structural: the old loader modeled a proposal that predated async/await, so the module-graph evaluation order that top-level await requires was never a shape the original design could hold cleanly. Translating the algorithm into C++ against the current specification gets the ordering right by construction rather than by patch.

Verification is the half of the post I would flag second. The team fuzzed complex module graphs, ran the output through the new loader, and compared it against other engines. "If the text output matched byte-for-byte with other engines' outputs, we could be sure that the new module loader was handling the test case correctly." They also mention feeding in the test262 and Web Platform Tests suites.

What this means for you today

If you author ES modules that use top-level await — the static kind at the head of a module, or import() of modules that use it — and you have hit ordering weirdness only in Safari, this is the fix you have been waiting on. On Safari 27 the two-file reproduction above should log 1, 2, 3 in order.

A few honest caveats. The WebKit post is scoped to Safari; it does not make claims about Baseline status or the state of other engines, and I am not going to invent them here. It also does not publish before-and-after performance numbers — the framing is correctness first, with native C++ described as more stable and predictable than the self-hosted JavaScript path.

What I am watching next

Two things. First, whether the rewrite makes it easier for WebKit to keep pace with the rest of the modules chapter of the ECMAScript spec — the point of aligning to pseudocode is that future changes become a translation exercise, not a re-architecture. Second, how the new loader behaves with import maps and dynamic import in the corners the post did not walk through. If you have a real app that leans on top-level await, this is a good week to open Safari and re-run the module-graph tests you gave up on.

Top comments (0)