Repository: facebook/react
Pull request: [Fiber] Name content inside "Suspense fallback"

Same as #33723 but for Fiber.

Changed files:
filename: packages/react-reconciler/src/ReactFiberComponentStack.js
patch: @@ -34,7 +34,7 @@ import {
 } from 'shared/ReactComponentStackFrame';
 import {formatOwnerStack} from 'shared/ReactOwnerStackFrames';
 
-function describeFiber(fiber: Fiber): string {
+function describeFiber(fiber: Fiber, childFiber: null | Fiber): string {
   switch (fiber.tag) {
     case HostHoistable:
     case HostSingleton:
@@ -44,6 +44,10 @@ function describeFiber(fiber: Fiber): string {
       // TODO: When we support Thenables as component types we should rename this.
       return describeBuiltInComponentFrame('Lazy');
     case SuspenseComponent:
+      if (fiber.child !== childFiber && childFiber !== null) {
+        // If we came from the second Fiber then we're in the Suspense Fallback.
+        return describeBuiltInComponentFrame('Suspense Fallback');
+      }
       return describeBuiltInComponentFrame('Suspense');
     case SuspenseListComponent:
       return describeBuiltInComponentFrame('SuspenseList');
@@ -70,8 +74,9 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
   try {
     let info = '';
     let node: Fiber = workInProgress;
+    let previous: null | Fiber = null;
     do {
-      info += describeFiber(node);
+      info += describeFiber(node, previous);
       if (__DEV__) {
         // Add any Server Component stack frames in reverse order.
         const debugInfo = node._debugInfo;
@@ -88,6 +93,7 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
           }
         }
       }
+      previous = node;
       // $FlowFixMe[incompatible-type] we bail out when we get a null
       node = node.return;
     } while (node);

filename: packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js
patch: @@ -87,7 +87,7 @@ describe('ReactFragment', () => {
   function normalizeCodeLocInfo(str) {
     return (
       str &&
-      str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
+      str.replace(/\n +(?:at|in) ([^\(]+) [^\n]*/g, function (m, name) {
         return '\n    in ' + name + ' (at **)';
       })
     );
@@ -168,6 +168,40 @@ describe('ReactFragment', () => {
     ]);
   });
 
+  it('includes built-in for Suspense fallbacks', async () => {
+    const SomethingThatSuspends = React.lazy(() => {
+      return new Promise(() => {});
+    });
+
+    ReactNoop.createRoot({
+      onCaughtError,
+    }).render(
+      <CatchingBoundary>
+        <Suspense fallback={<SomethingThatErrors />}>
+          <SomethingThatSuspends />
+        </Suspense>
+      </CatchingBoundary>,
+    );
+    await waitForAll([]);
+    expect(didCatchErrors).toEqual([
+      'uh oh',
+      componentStack([
+        'SomethingThatErrors',
+        'Suspense Fallback',
+        'CatchingBoundary',
+      ]),
+    ]);
+    expect(rootCaughtErrors).toEqual([
+      'uh oh',
+      componentStack([
+        'SomethingThatErrors',
+        'Suspense Fallback',
+        'CatchingBoundary',
+      ]),
+      __DEV__ ? componentStack(['SomethingThatErrors']) : null,
+    ]);
+  });
+
   // @gate enableActivity
   it('includes built-in for Activity', async () => {
     ReactNoop.createRoot({