Bun Runtime Missing AsyncLocalStorage Context Causes False Unhandled Rejection Logs For Dynamic Hooks In Next.Js 16.4.0
Under Bun 1.4.2, Next.js 16.4.0 logs false 'unhandledRejection' errors for CLIENT_HOOK_DYNAMIC because the rejection filter relies solely on workUnitAsyncStorage.getStore(), which Bun does not restore in unhandledRejection listeners. This produces hundreds of build log errors despite successful builds and can hide real failures.
The filteringUnhandledRejectionHandler in dist/server/node-environment-extensions/unhandled-rejection.external.js only checks workUnitAsyncStorage.getStore() and renderSignal.aborted. Node restores AsyncLocalStorage context inside unhandledRejection callbacks, but Bun 1.4.2 does not, so getStore() returns undefined and the filter forwards all rejections, including hanging promise rejections deliberately created for aborted prerenders.
1. In next.config.ts set cacheComponents: true and do not set partialPrefetching. 2. In root layout wrap a client component that calls usePathname() inside a Suspense boundary with fallback={null}. 3. Add a dynamic route such as app/hiives/[hiiveSlug]/events/[eventSlug]/book/page.tsx. 4. Run 'bun --bun next build' or 'bun --bun next dev' and load a dynamic route cold while another request to the same route is in flight. 5. Observe hundreds of false errors: 'Error: Route ... encountered URL data usePathname() in a Client Component outside of <Suspense>.' (digest: CLIENT_HOOK_DYNAMIC) despite the component being inside Suspense.
Fixing Code Block
// Replace filteringUnhandledRejectionHandler in dist/server/node-environment-extensions/unhandled-rejection.external.js
function filteringUnhandledRejectionHandler(reason, promise) {
const workUnitStore = workUnitAsyncStorage.getStore();
if (workUnitStore) {
if (
workUnitStore.type === 'prerender' ||
workUnitStore.type === 'prerender-ppr' ||
workUnitStore.type === 'prerender-legacy'
) {
if (workUnitStore.renderSignal.aborted) {
return;
}
}
} else if (
typeof reason === 'object' &&
reason !== null &&
(reason.digest === 'HANGING_PROMISE_REJECTION' ||
reason.digest === 'CLIENT_HOOK_DYNAMIC')
) {
// Runtime did not restore ALS context (e.g. Bun); these digests only reject on aborted renders.
return;
}
if (isDynamicUsageError(reason)) {
console.error(reason);
} else {
throw reason;
}
}
The fallback checks the rejection reason's digest when no AsyncLocalStorage store is available. HANGING_PROMISE_REJECTION and CLIENT_HOOK_DYNAMIC are only used by hanging promises created for deliberately aborted prerenders, so they can be safely swallowed. This removes false positives while preserving error reporting for genuine unhandled rejections.
Edge Case Audit
This hotfix could hide legitimate errors if these digests are ever reused for non-abort rejection reasons in future Next.js or React versions. Rollback by removing the else-if branch. It should be tested under Node, Bun, and Turbopack workers; consider adding a unit test that simulates missing ALS context. Also monitor Bun releases for AsyncLocalStorage restoration and remove the fallback once fixed upstream.