diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js index 964cf4509df6d..194d912331172 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -311,6 +311,7 @@ function render(children: React$Element, options?: Options): Destination { children, null, null, + null, options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, options ? options.onAllReady : undefined, diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index daa77239bad41..dc7266f54267f 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -4151,6 +4151,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, @@ -5373,7 +5394,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) { diff --git a/packages/react-server/src/__tests__/ReactServer-test.js b/packages/react-server/src/__tests__/ReactServer-test.js index d827e82de0cac..c6504fd21f578 100644 --- a/packages/react-server/src/__tests__/ReactServer-test.js +++ b/packages/react-server/src/__tests__/ReactServer-test.js @@ -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(
hello world
); expect(result.root).toEqual(div('hello world')); }); + + it('has Owner Stacks in DEV when aborted', async () => { + function Component({promise}) { + React.use(promise); + return
Hello, Dave!
; + } + function App({promise}) { + return ; + } + + let caughtError; + let componentStack; + let ownerStack; + const result = ReactNoopServer.render( + {})} />, + { + 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, + ); + }); });