Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
420ec2f2eb Remove test from process-on.test.ts (moved to 24069.test.ts) 2025-10-26 07:23:42 +00:00
Claude Bot
5eb383449a Fix panic when exceptions occur in uncaughtException handlers
When an exception is thrown inside an uncaughtException event handler,
Bun would panic with "Uncaught exception while handling uncaught exception".
This is particularly problematic in worker contexts.

The fix replaces the panic with a clean exit (code 7), matching Node.js behavior.

Changes:
- In VirtualMachine.zig uncaughtException(), when is_handling_uncaught_exception
  is true, log the error and call globalExit() instead of panicking

Fixes the crash seen when using @happy-dom/server-renderer and other packages
that handle exceptions in uncaughtException handlers in worker threads.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 07:22:20 +00:00
4 changed files with 96 additions and 2 deletions

View File

@@ -656,9 +656,12 @@ pub fn uncaughtException(this: *jsc.VirtualMachine, globalObject: *JSGlobalObjec
}
if (this.is_handling_uncaught_exception) {
// Nested exception while handling an uncaught exception
// Log it and exit without panicking
this.runErrorHandler(err, null);
bun.api.node.process.exit(globalObject, 7);
@panic("Uncaught exception while handling uncaught exception");
this.exit_handler.exit_code = 7;
this.onExit();
this.globalExit();
}
if (this.exit_on_uncaught_exception) {
this.runErrorHandler(err, null);
@@ -677,6 +680,10 @@ pub fn uncaughtException(this: *jsc.VirtualMachine, globalObject: *JSGlobalObjec
return handled;
}
pub export fn Bun__VM__isHandlingUncaughtException(vm: *jsc.VirtualMachine) callconv(.C) bool {
return vm.is_handling_uncaught_exception;
}
pub fn reportExceptionInHotReloadedModuleIfNeeded(this: *jsc.VirtualMachine) void {
defer this.addMainToWatcherIfNeeded();
var promise = this.pending_internal_promise orelse return;

View File

@@ -1101,7 +1101,14 @@ extern "C" int Bun__handleUncaughtException(JSC::JSGlobalObject* lexicalGlobalOb
auto uncaughtExceptionMonitor = Identifier::fromString(JSC::getVM(globalObject), "uncaughtExceptionMonitor"_s);
if (wrapped.listenerCount(uncaughtExceptionMonitor) > 0) {
auto scope = DECLARE_CATCH_SCOPE(vm);
wrapped.emit(uncaughtExceptionMonitor, args);
if (auto ex = scope.exception()) {
scope.clearException();
// if an exception is thrown in the uncaughtExceptionMonitor handler, we abort
Bun__logUnhandledException(JSValue::encode(JSValue(ex)));
Bun__Process__exit(lexicalGlobalObject, 1);
}
}
auto uncaughtExceptionIdent = Identifier::fromString(JSC::getVM(globalObject), "uncaughtException"_s);
@@ -1118,7 +1125,14 @@ extern "C" int Bun__handleUncaughtException(JSC::JSGlobalObject* lexicalGlobalOb
Bun__Process__exit(lexicalGlobalObject, 1);
}
} else if (wrapped.listenerCount(uncaughtExceptionIdent) > 0) {
auto scope = DECLARE_CATCH_SCOPE(vm);
wrapped.emit(uncaughtExceptionIdent, args);
if (auto ex = scope.exception()) {
scope.clearException();
// if an exception is thrown in the uncaughtException handler, we abort
Bun__logUnhandledException(JSValue::encode(JSValue(ex)));
Bun__Process__exit(lexicalGlobalObject, 1);
}
} else {
return false;
}

View File

@@ -14,6 +14,12 @@
#include <wtf/Vector.h>
#include <wtf/TZoneMallocInlines.h>
#include "ZigGlobalObject.h"
extern "C" bool Bun__VM__isHandlingUncaughtException(void* vm);
extern "C" void Bun__logUnhandledException(JSC::EncodedJSValue exception);
extern "C" void Bun__Process__exit(JSC::JSGlobalObject* globalObject, uint8_t exitCode);
namespace WebCore {
WTF_MAKE_TZONE_ALLOCATED_IMPL(EventEmitter);
@@ -257,6 +263,19 @@ bool EventEmitter::innerInvokeEventListeners(const Identifier& eventType, Simple
auto hasErrorListener = this->hasActiveEventListeners(errorIdentifier);
if (!hasErrorListener || eventType == errorIdentifier) {
// If the event type is error, report the exception to the console.
// However, if we're already handling an uncaught exception, don't report it again
// to avoid infinite recursion and panic. Instead, log and exit.
if (lexicalGlobalObject->inherits(Zig::GlobalObject::info())) {
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
auto bunVM = globalObject->bunVM();
if (bunVM && Bun__VM__isHandlingUncaughtException(bunVM)) {
// Already handling an uncaught exception - log this nested exception and exit
// This matches Node.js behavior
Bun__logUnhandledException(JSValue::encode(JSValue(exception)));
Bun__Process__exit(lexicalGlobalObject, 7);
return fired;
}
}
Bun__reportUnhandledError(lexicalGlobalObject, JSValue::encode(JSValue(exception)));
} else if (hasErrorListener) {
MarkedArgumentBuffer expcep;

View File

@@ -0,0 +1,54 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
test("should not panic when exception is thrown in uncaughtException handler in worker", async () => {
const dir = tempDirWithFiles("uncaught-exception-worker-test", {
"worker.js": `
const observedWindows = [{ Error: undefined }];
process.on('uncaughtException', (error) => {
// This mimics what happy-dom does - checking instanceof with undefined
// This will throw TypeError: Right hand side of instanceof is not an object
for (const window of observedWindows) {
if (error instanceof window.Error) {
break;
}
}
});
throw new Error("Test error");
`,
"main.js": `
import { Worker } from 'worker_threads';
const worker = new Worker('./worker.js');
worker.on('exit', (code) => {
// Worker should exit with code 7 (nested exception)
process.exit(code === 7 ? 0 : 1);
});
setTimeout(() => process.exit(1), 5000);
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "main.js"],
cwd: String(dir),
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
// Should not panic
expect(stderr).not.toContain("panic:");
expect(stderr).not.toContain("oh no: Bun has crashed");
// Should show the TypeError
expect(stderr).toContain("TypeError");
expect(stderr).toContain("Right hand side of instanceof is not an object");
// Should exit 0 (worker exited with 7, main treats that as success)
expect(exitCode).toBe(0);
});