Repository: vercel/next.js
Pull request: Improve reliability of owner stacks for async I/O errors

When running the following test isolated, and not as part of the full test suite, the owner stacks for async I/O errors were missing the top-most stack frame that's pointing at the `fetch` call.

```
pnpm test-dev test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.test.ts -t "should show a collapsed redbox with two errors"
```

The same could be reproduced with `pnpm next dev test/e2e/app-dir/dynamic-io-errors/fixtures/default` at http://localhost:3000/dynamic-root, unless http://localhost:3000/static was visited first.

The likely reason for that is that React's async I/O tracking was recently optimized for performance reasons in facebook/react#33736 and facebook/react#33737.

We can help React a bit with the tracking by explicitly awaiting our `makeHangingPromise` calls. With this fix, the case from above reliably works in isolation.

Changed files:
filename: packages/next/src/server/lib/patch-fetch.ts
patch: @@ -559,7 +559,7 @@ export function createPatchedFetcher(
                 cacheSignal = null
               }
 
-              return makeHangingPromise<Response>(
+              return await makeHangingPromise<Response>(
                 workUnitStore.renderSignal,
                 'fetch()'
               )
@@ -669,7 +669,7 @@ export function createPatchedFetcher(
                     cacheSignal.endRead()
                     cacheSignal = null
                   }
-                  return makeHangingPromise<Response>(
+                  return await makeHangingPromise<Response>(
                     workUnitStore.renderSignal,
                     'fetch()'
                   )
@@ -1010,7 +1010,7 @@ export function createPatchedFetcher(
                     cacheSignal.endRead()
                     cacheSignal = null
                   }
-                  return makeHangingPromise<Response>(
+                  return await makeHangingPromise<Response>(
                     workUnitStore.renderSignal,
                     'fetch()'
                   )
@@ -1044,7 +1044,7 @@ export function createPatchedFetcher(
                 switch (workUnitStore.type) {
                   case 'prerender':
                   case 'prerender-client':
-                    return makeHangingPromise<Response>(
+                    return await makeHangingPromise<Response>(
                       workUnitStore.renderSignal,
                       'fetch()'
                     )

filename: packages/next/src/server/use-cache/use-cache-wrapper.ts
patch: @@ -821,7 +821,7 @@ export function cache(
             )
 
             if (dynamicAccessAbortController.signal.aborted) {
-              return makeHangingPromise(
+              return await makeHangingPromise(
                 workUnitStore.renderSignal,
                 dynamicAccessAbortController.signal.reason.message
               )
@@ -886,7 +886,7 @@ export function cache(
                 if (cacheSignal) {
                   cacheSignal.endRead()
                 }
-                return makeHangingPromise(
+                return await makeHangingPromise(
                   workUnitStore.renderSignal,
                   'dynamic "use cache"'
                 )
@@ -935,7 +935,7 @@ export function cache(
                 // transformed with an async function, before being passed into
                 // the "use cache" function, which escapes the instrumentation.
                 if (workUnitStore.allowEmptyStaticShell) {
-                  return makeHangingPromise(
+                  return await makeHangingPromise(
                     workUnitStore.renderSignal,
                     'dynamic "use cache"'
                   )
@@ -1029,7 +1029,7 @@ export function cache(
               if (cacheSignal) {
                 cacheSignal.endRead()
               }
-              return makeHangingPromise(
+              return await makeHangingPromise(
                 workUnitStore.renderSignal,
                 'dynamic "use cache"'
               )
@@ -1086,7 +1086,7 @@ export function cache(
           )
 
           if (result.type === 'prerender-dynamic') {
-            return result.hangingPromise
+            return await result.hangingPromise
           }
 
           const { stream: newStream, pendingCacheEntry } = result

filename: test/e2e/app-dir/dynamic-io-errors/dynamic-io-errors.test.ts
patch: @@ -554,10 +554,11 @@ describe('Dynamic IO Errors', () => {
                  "description": "Route "/dynamic-root": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
                  "environmentLabel": "Server",
                  "label": "Console Error",
-                 "source": "app/dynamic-root/page.tsx (45:56) @ FetchingComponent
-             > 45 |       {cached ? await fetchRandomCached(nonce) : await fetchRandom(nonce)}
-                  |                                                        ^",
+                 "source": "app/dynamic-root/page.tsx (59:26) @ fetchRandom
+             > 59 |   const response = await fetch(
+                  |                          ^",
                  "stack": [
+                   "fetchRandom app/dynamic-root/page.tsx (59:26)",
                    "FetchingComponent app/dynamic-root/page.tsx (45:56)",
                    "Page app/dynamic-root/page.tsx (22:9)",
                    "LogSafely <anonymous>",
@@ -567,10 +568,11 @@ describe('Dynamic IO Errors', () => {
                  "description": "Route "/dynamic-root": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense",
                  "environmentLabel": "Server",
                  "label": "Console Error",
-                 "source": "app/dynamic-root/page.tsx (45:56) @ FetchingComponent
-             > 45 |       {cached ? await fetchRandomCached(nonce) : await fetchRandom(nonce)}
-                  |                                                        ^",
+                 "source": "app/dynamic-root/page.tsx (59:26) @ fetchRandom
+             > 59 |   const response = await fetch(
+                  |                          ^",
                  "stack": [
+                   "fetchRandom app/dynamic-root/page.tsx (59:26)",
                    "FetchingComponent app/dynamic-root/page.tsx (45:56)",
                    "Page app/dynamic-root/page.tsx (27:7)",
                    "LogSafely <anonymous>",