From 661592f99383379dd67c27ca831eea1a654b8ff8 Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 30 Jun 2026 19:37:43 +0000 Subject: [PATCH] ModuleLoader: record a non-Error fetch rejection as a fetch failure In moduleLoadTopSettled's rejected branch, the entire fetch/evaluation-error classification is gated on dynamicDowncast. Bun rejects a single-message transpile failure with a BuildMessage, which is a plain JSDestructibleObject, so nothing is recorded on the registry entry at all. moduleLoadTopRejected then falls through its fetchError() check and records the value with setEvaluationError. That leaves the entry at EvaluationFailed with no fetch, module, or load promise. The next importer of the same key that goes through hostLoadImportedModule (a static import, or a dynamic import inside another module's graph load) finds a non-New entry with no fetchError and no loadPromise, creates a fresh pending fetchPromise via ensureModulePromise, and waits on it forever. Nothing ever settles it. // bad.ts contains "import {" (exactly one parser error, so the // rejection value is a BuildMessage, not an AggregateError) await import("./bad.ts").catch(() => {}); await import("./other.ts"); // other.ts statically imports bad.ts; hangs This microtask reacts to the embedder fetch promise, so every rejection that reaches it is a fetch failure by construction: the promise is either a fresh fetch() or a cached entry->ensureFetchPromise(), and the only rejector of an entry's fetch promise is setFetchError. Add the else branch so a non-Error rejection value is recorded the same way. This matches what the sibling nested-import path already does: moduleRegistryFetchSettled calls setFetchError unconditionally and only gates the attachErrorInfo metadata on the value being an ErrorInstance. With the entry at FetchFailed, moduleLoadTopRejected rejects with the cached fetchError() instead of calling setEvaluationError, and later importers take hostLoadImportedModule's fetchError short circuit instead of parking on a promise nothing settles. Independent of oven-sh/WebKit#258 (the import-attribute module map key): that change threads the attribute into ensureRegistered here but keeps the ErrorInstance gate, and this hang reproduces with no import attributes at all. The two apply cleanly together. --- Source/JavaScriptCore/runtime/JSMicrotask.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index 3cb3b7e9a996..6ada5786e7cb 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -1124,6 +1124,11 @@ static void moduleLoadTopSettled(JSGlobalObject* globalObject, VM& vm, ThrowScop entry->setEvaluationError(globalObject, error); else entry->setFetchError(globalObject, error); + } else { + // This microtask reacts to the embedder fetch promise, so a rejection + // here is a fetch failure even when the host rejected with a non-Error + // value (matching moduleRegistryFetchSettled's unconditional setFetchError). + entry->setFetchError(globalObject, errorValue); } intermediatePromise->reject(vm, errorValue); }