Repository: facebook/react
Pull request: [Fizz] Add Owner Stacks when render is aborted



## Summary

`captureOwnerStack` now returns the corresponding owner stack to the parent stack when we abort a Fizz render.

## How did you test this change?

- added test (initial commit has current behavior)


Changed files:
filename: packages/react-noop-renderer/src/ReactNoopServer.js
patch: @@ -364,6 +364,7 @@ function render(children: React$Element<any>, options?: Options): Destination {
     children,
     null,
     null,
+    null,
     options ? options.progressiveChunkSize : undefined,
     options ? options.onError : undefined,
     options ? options.onAllReady : undefined,

filename: packages/react-server/src/ReactFizzServer.js
patch: @@ -4762,6 +4762,27 @@ function abortTask(task: Task, request: Request, error: mixed): void {
   }
 }
 
+function abortTaskDEV(task: Task, request: Request, error: mixed): void {
+  if (__DEV__) {
+    const prevTaskInDEV = currentTaskInDEV;
+    const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;
+    setCurrentTaskInDEV(task);
+    ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
+    try {
+      abortTask(task, request, error);
+    } finally {
+      setCurrentTaskInDEV(prevTaskInDEV);
+      ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;
+    }
+  } else {
+    // These errors should never make it into a build so we don't need to encode them in codes.json
+    // eslint-disable-next-line react-internal/prod-error-codes
+    throw new Error(
+      'abortTaskDEV should never be called in production mode. This is a bug in React.',
+    );
+  }
+}
+
 function safelyEmitEarlyPreloads(
   request: Request,
   shellComplete: boolean,
@@ -6111,7 +6132,11 @@ export function abort(request: Request, reason: mixed): void {
       // This error isn't necessarily fatal in this case but we need to stash it
       // so we can use it to abort any pending work
       request.fatalError = error;
-      abortableTasks.forEach(task => abortTask(task, request, error));
+      if (__DEV__) {
+        abortableTasks.forEach(task => abortTaskDEV(task, request, error));
+      } else {
+        abortableTasks.forEach(task => abortTask(task, request, error));
+      }
       abortableTasks.clear();
     }
     if (request.destination !== null) {

filename: packages/react-server/src/__tests__/ReactServer-test.js
patch: @@ -10,13 +10,28 @@
 
 'use strict';
 
+let act;
 let React;
 let ReactNoopServer;
 
+function normalizeCodeLocInfo(str) {
+  return (
+    str &&
+    str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
+      const dot = name.lastIndexOf('.');
+      if (dot !== -1) {
+        name = name.slice(dot + 1);
+      }
+      return '    in ' + name + (/\d/.test(m) ? ' (at **)' : '');
+    })
+  );
+}
+
 describe('ReactServer', () => {
   beforeEach(() => {
     jest.resetModules();
 
+    act = require('internal-test-utils').act;
     React = require('react');
     ReactNoopServer = require('react-noop-renderer/server');
   });
@@ -32,4 +47,43 @@ describe('ReactServer', () => {
     const result = ReactNoopServer.render(<div>hello world</div>);
     expect(result.root).toEqual(div('hello world'));
   });
+
+  it('has Owner Stacks in DEV when aborted', async () => {
+    function Component({promise}) {
+      React.use(promise);
+      return <div>Hello, Dave!</div>;
+    }
+    function App({promise}) {
+      return <Component promise={promise} />;
+    }
+
+    let caughtError;
+    let componentStack;
+    let ownerStack;
+    const result = ReactNoopServer.render(
+      <App promise={new Promise(() => {})} />,
+      {
+        onError: (error, errorInfo) => {
+          caughtError = error;
+          componentStack = errorInfo.componentStack;
+          ownerStack = __DEV__ ? React.captureOwnerStack() : null;
+        },
+      },
+    );
+
+    await act(async () => {
+      result.abort();
+    });
+    expect(caughtError).toEqual(
+      expect.objectContaining({
+        message: 'The render was aborted by the server without a reason.',
+      }),
+    );
+    expect(normalizeCodeLocInfo(componentStack)).toEqual(
+      '\n    in Component (at **)' + '\n    in App (at **)',
+    );
+    expect(normalizeCodeLocInfo(ownerStack)).toEqual(
+      __DEV__ ? '\n    in App (at **)' : null,
+    );
+  });
 });