diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 100aa7a6cc..47ba04812e 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -113,6 +113,12 @@ pub fn drainMicrotasksWithGlobal(this: *EventLoop, globalObject: *JSC.JSGlobalOb jsc_vm.releaseWeakRefs(); JSC__JSGlobalObject__drainMicrotasks(globalObject); + // Check if an exception was thrown during microtask execution + if (globalObject.hasException()) { + // Report the exception as unhandled - this will clear the exception and report it + globalObject.reportActiveExceptionAsUnhandled(error.JSError); + } + this.virtual_machine.is_inside_deferred_task_queue = true; this.deferred_tasks.run(); this.virtual_machine.is_inside_deferred_task_queue = false; diff --git a/test/js/web/timers/microtask-exception.test.js b/test/js/web/timers/microtask-exception.test.js new file mode 100644 index 0000000000..c485374b82 --- /dev/null +++ b/test/js/web/timers/microtask-exception.test.js @@ -0,0 +1,75 @@ +import { it, expect } from "bun:test"; + +it("queueMicrotask exception handling", async () => { + // Test that exceptions in microtasks are properly reported and don't crash + const errors = []; + const originalOnError = globalThis.onerror; + + // Set up error handler to capture unhandled exceptions + globalThis.onerror = (message, source, lineno, colno, error) => { + errors.push({ message, error }); + return true; // Prevent default error handling + }; + + try { + await new Promise(resolve => { + let microtaskRan = false; + + // Queue a microtask that throws + queueMicrotask(() => { + throw new Error("Exception from microtask!"); + }); + + // Queue another microtask to verify execution continues + queueMicrotask(() => { + microtaskRan = true; + }); + + // Wait a bit for microtasks to run + setTimeout(() => { + expect(microtaskRan).toBe(true); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].error.message).toBe("Exception from microtask!"); + resolve(); + }, 10); + }); + } finally { + // Restore original error handler + globalThis.onerror = originalOnError; + } +}); + +it("process.nextTick exception handling", async () => { + // Test that exceptions in nextTick callbacks are properly reported + const errors = []; + const originalOnError = globalThis.onerror; + + globalThis.onerror = (message, source, lineno, colno, error) => { + errors.push({ message, error }); + return true; + }; + + try { + await new Promise(resolve => { + let nextTickRan = false; + + // Use nextTick which also uses the microtask queue + process.nextTick(() => { + throw new Error("Exception from nextTick!"); + }); + + process.nextTick(() => { + nextTickRan = true; + }); + + setTimeout(() => { + expect(nextTickRan).toBe(true); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].error.message).toBe("Exception from nextTick!"); + resolve(); + }, 10); + }); + } finally { + globalThis.onerror = originalOnError; + } +});