Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1324,9 +1324,8 @@ extern "C" void Bun__logUnhandledException(JSC::EncodedJSValue exception);

extern "C" int Bun__handleUncaughtException(JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue exception, int isRejection)
{
if (!lexicalGlobalObject->inherits(Zig::GlobalObject::info()))
return false;
auto* globalObject = uncheckedDowncast<Zig::GlobalObject>(lexicalGlobalObject);
// The exception may come from a node:vm context, which has no process of its own.
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
auto* process = globalObject->processObject();
auto& wrapped = process->wrapped();
auto& vm = JSC::getVM(globalObject);
Expand Down Expand Up @@ -1372,14 +1371,14 @@ extern "C" int Bun__handleUncaughtException(JSC::JSGlobalObject* lexicalGlobalOb
auto capture = process->getUncaughtExceptionCaptureCallback();
if (!capture.isEmpty() && !capture.isUndefinedOrNull()) {
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
(void)call(lexicalGlobalObject, capture, args, "uncaughtExceptionCaptureCallback"_s);
(void)call(globalObject, capture, args, "uncaughtExceptionCaptureCallback"_s);
if (auto ex = scope.exception()) {
(void)scope.tryClearException();
if (vm.hasPendingTerminationException()) [[unlikely]]
return true;
// if an exception is thrown in the uncaughtException handler, we abort
Bun__logUnhandledException(JSValue::encode(JSValue(ex)));
Bun__Process__exit(lexicalGlobalObject, 1);
Bun__Process__exit(globalObject, 1);
}
} else if (wrapped.listenerCount(uncaughtExceptionIdent) > 0) {
auto emitScope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
Expand Down
9 changes: 2 additions & 7 deletions src/jsc/bindings/JSDOMExceptionHandling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,12 @@ void reportException(JSGlobalObject* lexicalGlobalObject, JSC::Exception* except
(void)scope.tryClearException();
vm.clearLastException();

auto* globalObject = uncheckedDowncast<JSDOMGlobalObject>(lexicalGlobalObject);
// if (auto* window = dynamicDowncast<JSDOMWindow>( globalObject)) {
// if (!window->wrapped().isCurrentlyDisplayedInFrame())
// return;
// }

int lineNumber = 0;
int columnNumber = 0;
String exceptionSourceURL;

Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception);
// The listener's realm may be a node:vm context, not a Zig::GlobalObject.
Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(lexicalGlobalObject, exception);
RETURN_IF_EXCEPTION(scope, );

if (exceptionDetails) {
Expand Down
5 changes: 4 additions & 1 deletion src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1158,7 +1158,7 @@ impl WebWorker {

fn on_unhandled_rejection(
vm: &mut VirtualMachine,
global_object: &JSGlobalObject,
_: &JSGlobalObject,
error_instance_or_exception: JSValue,
) {
// Prevent recursion
Expand All @@ -1172,6 +1172,9 @@ fn on_unhandled_rejection(
return;
}

// Not the realm the error was raised in: that may be a node:vm context.
let global_object = vm.global();

let mut error_instance = error_instance_or_exception
.to_error()
.unwrap_or(error_instance_or_exception);
Expand Down
96 changes: 96 additions & 0 deletions test/js/node/vm/vm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2551,3 +2551,99 @@ test.skipIf(memoryForLongStrings < 10 * 1024 ** 3)(
},
30_000,
);

// A vm context has no process object of its own, so an exception the runtime reports against one (the
// realm of a FinalizationRegistry or of a listener function created in the context) goes to this thread's
// process, as in Node. Node prints the same lines and exits 0 for each of these children.
describe("an exception reported against a vm context reaches the process uncaughtException handlers", () => {
const thrower = `vm.runInNewContext('(function () { throw new Error("thrown in vm context"); })')`;
const dispatchToListenerFromContext = `
const target = new EventTarget();
target.addEventListener("ping", ${thrower});
target.dispatchEvent(new Event("ping"));
`;
const listener = `process.on("uncaughtException", (err, origin) => console.log("uncaughtException:", err.message, origin));`;

// The monitor on the main thread's process doubles as the check that the worker children below do
// not report to the wrong thread.
async function run(consumer: string, trigger: string) {
const code = `
const vm = require("node:vm");
process.on("uncaughtExceptionMonitor", (err, origin) => console.log("monitor:", err.message, origin));
${consumer}
${trigger}
`;
await using proc = Bun.spawn({ cmd: [bunExe(), "-e", code], env: bunEnv, stdout: "pipe", stderr: "pipe" });
return await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
}

test.concurrent.each([
[
"a FinalizationRegistry created in the context",
`
let ran = false;
const context = vm.createContext({ markRan: () => { ran = true; } });
vm.runInContext(\`
globalThis.registry = new FinalizationRegistry(() => { markRan(); throw new Error("thrown in vm context"); });
registry.register({}, 1);
\`, context);
for (let i = 0; i < 1000 && !ran; i++) {
Bun.gc(true);
await Bun.sleep(1);
}
if (!ran) console.log("the cleanup callback never ran");
`,
],
["a process 'exit' listener created in a context", `process.on("exit", ${thrower});`],
["an EventTarget listener created in a context", dispatchToListenerFromContext],
])("%s", async (_, trigger) => {
const [stdout, stderr, exitCode] = await run(listener, trigger);
expect(stderr).toBe("");
expect(stdout).toBe(
"monitor: thrown in vm context uncaughtException\nuncaughtException: thrown in vm context uncaughtException\n",
);
expect(exitCode).toBe(0);
});

test.concurrent("process.setUncaughtExceptionCaptureCallback() receives it too", async () => {
const [stdout, stderr, exitCode] = await run(
`process.setUncaughtExceptionCaptureCallback(err => console.log("captured:", err.message));`,
dispatchToListenerFromContext,
);
expect(stderr).toBe("");
expect(stdout).toBe("monitor: thrown in vm context uncaughtException\ncaptured: thrown in vm context\n");
expect(exitCode).toBe(0);
});

// In a worker the exception belongs to the worker's process. Without a handler there it becomes the
// Worker's 'error' event, as in Node.
test.concurrent.each([
[
"the worker's uncaughtException handler",
`process.on("uncaughtException", (err, origin) => console.log("worker uncaughtException:", err.message, origin));`,
"worker uncaughtException: thrown in vm context uncaughtException\nworker exit: 0\n",
],
[
"the Worker 'error' event when the worker has no handler",
"",
"worker error event: thrown in vm context\nworker exit: 1\n",
],
])("in a worker it reaches %s", async (_, workerConsumer, expected) => {
const [stdout, stderr, exitCode] = await run(
"",
`
const { Worker } = require("node:worker_threads");
const worker = new Worker(\`
const vm = require("node:vm");
${workerConsumer}
${dispatchToListenerFromContext}
\`, { eval: true });
worker.on("error", err => console.log("worker error event:", err.message));
worker.on("exit", code => console.log("worker exit:", code));
`,
);
expect(stderr).toBe("");
expect(stdout).toBe(expected);
expect(exitCode).toBe(0);
});
});
Loading