fix: clean up ESM registry when require() of ESM module fails (#27288)

## Summary

- When `require()` loads an ESM module (`.mjs`) that throws during
evaluation, the module was removed from `requireMap` but left in the ESM
registry (`Loader.registry`) in a partially-initialized state
- A subsequent `import()` of the same module would find this corrupt
entry and throw `ReferenceError: Cannot access 'foo' before
initialization` instead of re-throwing the original evaluation error
- Fix by also deleting the module from `Loader.registry` in both
`overridableRequire` and `requireESMFromHijackedExtension` when ESM
evaluation fails, allowing `import()` to re-evaluate from scratch

Closes #27287

## Test plan

- [x] Added regression test in `test/regression/issue/27287.test.ts`
- [x] Verified test fails with system bun (`USE_SYSTEM_BUN=1`)
- [x] Verified test passes with `bun bd test`
- [x] Manual verification: `require()` correctly throws original error,
`import()` now re-throws the same error instead of `ReferenceError`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alistair Smith <alistair@anthropic.com>
This commit is contained in:
robobun
2026-02-21 01:08:50 +00:00
committed by GitHub
parent 7baf50f379
commit 21c3439bb4
2 changed files with 59 additions and 0 deletions

View File

@@ -108,6 +108,11 @@ export function overridableRequire(this: JSCommonJSModule, originalId: string, o
} catch (exception) {
// Since the ESM code is mostly JS, we need to handle exceptions here.
$requireMap.$delete(id);
// Also remove the failed module from the ESM registry so that
// a subsequent import() can re-evaluate it from scratch instead
// of finding the partially-initialized module entry.
// https://github.com/oven-sh/bun/issues/27287
Loader.registry.$delete(id);
throw exception;
}
@@ -321,6 +326,11 @@ export function requireESMFromHijackedExtension(this: JSCommonJSModule, id: stri
} catch (exception) {
// Since the ESM code is mostly JS, we need to handle exceptions here.
$requireMap.$delete(id);
// Also remove the failed module from the ESM registry so that
// a subsequent import() can re-evaluate it from scratch instead
// of finding the partially-initialized module entry.
// https://github.com/oven-sh/bun/issues/27287
Loader.registry.$delete(id);
throw exception;
}

View File

@@ -0,0 +1,49 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/27287
test("CJS require() of failing ESM does not corrupt module for subsequent import()", async () => {
using dir = tempDir("issue-27287", {
"bad-esm.mjs": `throw globalThis.err;\nexport const foo = 2;\n`,
"entry.cjs": `
'use strict';
globalThis.err = new Error('intentional error');
// First: require() the failing ESM module
try {
require('./bad-esm.mjs');
} catch (e) {
console.log('require_error:', e.message);
}
// Second: import() the same module - should re-throw the original error, not ReferenceError
import('./bad-esm.mjs')
.then(() => {
console.log('import_result: resolved');
})
.catch((e) => {
console.log('import_error_type:', e.constructor.name);
console.log('import_error_msg:', e.message);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "entry.cjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("require_error: intentional error");
// The import() should re-throw the original evaluation error, NOT a ReferenceError
// about uninitialized exports. The module threw during evaluation, so import() should
// reject with the same error.
expect(stdout).not.toContain("ReferenceError");
expect(stdout).toContain("import_error_type: Error");
expect(stdout).toContain("import_error_msg: intentional error");
expect(exitCode).toBe(0);
});