From ecfd4e78d2d73190d4609522717575d00b99c94a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:48:48 +0000 Subject: [PATCH 1/2] Bun.sql getters: stop reporting a pending exception as uncaught in debug builds defaultBunSQLObject and constructBunSQLObject called reportUncaughtExceptionAtEventLoop while the exception thrown by requireId was still pending on the VM. That runs the uncaught exception machinery (process._fatalException lookup, error printing) with an exception set, which trips JSC's exception scope assertions and aborts the debug build, for example when Bun.sql is first touched with the stack nearly exhausted. It also marked a caught error as unhandled. The exception already propagates to the property access through reifyStaticProperty, so just let RETURN_IF_EXCEPTION handle it. --- src/jsc/bindings/BunObject.cpp | 6 ----- test/js/sql/sql.test.ts | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index 8a022228eb4c..e4d4ab407ba5 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -319,9 +319,6 @@ static JSValue defaultBunSQLObject(VM& vm, JSObject* bunObject) auto scope = DECLARE_THROW_SCOPE(vm); auto* globalObject = defaultGlobalObject(bunObject->globalObject()); JSValue sqlValue = globalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::BunSql); -#if BUN_DEBUG - if (scope.exception()) globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception()); -#endif RETURN_IF_EXCEPTION(scope, {}); RELEASE_AND_RETURN(scope, sqlValue.getObject()->get(globalObject, vm.propertyNames->defaultKeyword)); } @@ -331,9 +328,6 @@ static JSValue constructBunSQLObject(VM& vm, JSObject* bunObject) auto scope = DECLARE_THROW_SCOPE(vm); auto* globalObject = defaultGlobalObject(bunObject->globalObject()); JSValue sqlValue = globalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::BunSql); -#if BUN_DEBUG - if (scope.exception()) globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception()); -#endif RETURN_IF_EXCEPTION(scope, {}); auto clientData = WebCore::clientData(vm); RELEASE_AND_RETURN(scope, sqlValue.getObject()->get(globalObject, clientData->builtinNames().SQLPublicName())); diff --git a/test/js/sql/sql.test.ts b/test/js/sql/sql.test.ts index 75ce11d3b130..ed5a4d959e2e 100644 --- a/test/js/sql/sql.test.ts +++ b/test/js/sql/sql.test.ts @@ -12781,3 +12781,50 @@ test("data row that omits columns declared in the row description yields nulls f expect(filteredStderr).toBe(""); expect(exitCode).toBe(0); }, 30_000); + +// Bun.sql / Bun.postgres / Bun.SQL are lazy properties whose getter loads the +// bun:sql module on first access. If that load throws (here the stack is +// already exhausted, so evaluating the module overflows it), the throw has to +// reach the caller like any other exception and the property must stay lazy so +// a later access works. Debug builds used to additionally report the pending +// exception as uncaught from inside the getter, which aborted the process. +// Runs in a subprocess because the property must not have been reified yet. +test.concurrent.each(["sql", "postgres", "SQL"])("Bun.%s throwing under stack exhaustion is catchable", async name => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + let first; + function recurse() { + try { + recurse(); + } catch {} + if (first === undefined) { + try { + Bun.${name}; + } catch (e) { + first = e; + } + } + } + recurse(); + console.log(String(first)); + console.log(typeof Bun.${name}); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const filteredStderr = stderr + .split(/\r?\n/) + .filter(l => l && !l.startsWith("WARNING: ASAN interferes")) + .join("\n"); + + expect(stdout).toBe("RangeError: Maximum call stack size exceeded.\nfunction\n"); + expect(filteredStderr).toBe(""); + expect(exitCode).toBe(0); +}); From b51acc108cf9d0c8d27c4901436d519f7b5f1a57 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:56:35 +0000 Subject: [PATCH 2/2] test: assert the subprocess stderr directly --- test/js/sql/sql.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/js/sql/sql.test.ts b/test/js/sql/sql.test.ts index ed5a4d959e2e..6e435ac4ed81 100644 --- a/test/js/sql/sql.test.ts +++ b/test/js/sql/sql.test.ts @@ -12819,12 +12819,7 @@ test.concurrent.each(["sql", "postgres", "SQL"])("Bun.%s throwing under stack ex }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - const filteredStderr = stderr - .split(/\r?\n/) - .filter(l => l && !l.startsWith("WARNING: ASAN interferes")) - .join("\n"); - expect(stdout).toBe("RangeError: Maximum call stack size exceeded.\nfunction\n"); - expect(filteredStderr).toBe(""); + expect(stderr).toBe(""); expect(exitCode).toBe(0); });