diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index e1f3e7adc46..b0e47939a2c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -432,16 +432,25 @@ describe('ReactDOMFizzServer', () => { } const serverErrors = []; + const browserBailouts = []; await act(() => { const {pipe} = renderToPipeableStream(, { onError(error) { serverErrors.push(error); }, + onBrowserBailout(error, errorInfo) { + browserBailouts.push({error, errorInfo}); + }, }); pipe(writable); }); expect(serverErrors).toEqual([]); + expect(browserBailouts).toHaveLength(1); + expect(browserBailouts[0].error).toBe(browserOnly); + expect( + normalizeCodeLocInfo(browserBailouts[0].errorInfo.componentStack), + ).toBe(componentStack(['BrowserOnly', 'Suspense', 'div', 'App'])); expect(getVisibleChildren(container)).toEqual(
Fallback @@ -548,6 +557,7 @@ describe('ReactDOMFizzServer', () => { } const reportedErrors = []; + const browserBailouts = []; let shellReady = false; let shellError; await act(() => { @@ -555,6 +565,9 @@ describe('ReactDOMFizzServer', () => { onError(error) { reportedErrors.push(error); }, + onBrowserBailout(error) { + browserBailouts.push(error); + }, onShellReady() { shellReady = true; }, @@ -573,11 +586,9 @@ describe('ReactDOMFizzServer', () => { expect(shellError.stack).toContain('BrowserOnly'); expect(shellError.cause).toBe(browserValue); expect(shellError.cause.stack).toContain('createBrowserValue'); - expect(shellError.cause.message).toContain( - '`use(browser())` can only be used inside a `` boundary', - ); expect(shellReady).toBe(false); expect(reportedErrors).toEqual([shellError]); + expect(browserBailouts).toEqual([]); }); // @gate enableBrowserAPI @@ -607,12 +618,17 @@ describe('ReactDOMFizzServer', () => { } const serverErrors = []; + const browserBailouts = []; + const browserValue = ReactDOM.browser(); let abort; await act(() => { const controls = renderToPipeableStream(, { onError(error) { serverErrors.push(error); }, + onBrowserBailout(error) { + browserBailouts.push(error); + }, }); abort = controls.abort; controls.pipe(writable); @@ -627,10 +643,11 @@ describe('ReactDOMFizzServer', () => { ); await act(() => { - abort(ReactDOM.browser()); + abort(browserValue); }); expect(serverErrors).toEqual([]); + expect(browserBailouts).toEqual([browserValue, browserValue]); isClient = true; const recoverableErrors = []; @@ -662,6 +679,7 @@ describe('ReactDOMFizzServer', () => { } const reportedErrors = []; + const browserBailouts = []; let shellReady = false; let shellError; let abort; @@ -670,6 +688,9 @@ describe('ReactDOMFizzServer', () => { onError(error) { reportedErrors.push(error); }, + onBrowserBailout(error) { + browserBailouts.push(error); + }, onShellReady() { shellReady = true; }, @@ -693,6 +714,7 @@ describe('ReactDOMFizzServer', () => { expect(shellError.cause).toBe(browserValue); expect(shellReady).toBe(false); expect(reportedErrors).toEqual([shellError]); + expect(browserBailouts).toEqual([]); }); // @gate enableBrowserAPI @@ -704,6 +726,7 @@ describe('ReactDOMFizzServer', () => { } const reportedErrors = []; + const browserBailouts = []; await act(() => { const {pipe} = renderToPipeableStream( Fallback}> @@ -713,12 +736,16 @@ describe('ReactDOMFizzServer', () => { onError(error) { reportedErrors.push(error); }, + onBrowserBailout(error) { + browserBailouts.push(error); + }, }, ); pipe(writable); }); expect(reportedErrors).toEqual([browserValue]); + expect(browserBailouts).toEqual([]); expect(getVisibleChildren(container)).toEqual(Fallback); }); diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index 51c25c0f765..162e937f96d 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -53,6 +53,7 @@ type Options = { progressiveChunkSize?: number, signal?: AbortSignal, onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, formState?: ReactFormState | null, @@ -64,6 +65,7 @@ type ResumeOptions = { nonce?: NonceOption, signal?: AbortSignal, onError?: (error: mixed) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, }; @@ -141,6 +143,7 @@ function renderToReadableStream( createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, onShellReady, onShellError, @@ -211,6 +214,7 @@ function resume( options ? options.nonce : undefined, ), options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, onShellReady, onShellError, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBun.js b/packages/react-dom/src/server/ReactDOMFizzServerBun.js index 2d9c6d1edbc..8e3ce7a411c 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBun.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBun.js @@ -49,6 +49,7 @@ type Options = { progressiveChunkSize?: number, signal?: AbortSignal, onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, formState?: ReactFormState | null, @@ -131,6 +132,7 @@ function renderToReadableStream( createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, onShellReady, onShellError, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerEdge.js b/packages/react-dom/src/server/ReactDOMFizzServerEdge.js index 51c25c0f765..162e937f96d 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerEdge.js @@ -53,6 +53,7 @@ type Options = { progressiveChunkSize?: number, signal?: AbortSignal, onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, formState?: ReactFormState | null, @@ -64,6 +65,7 @@ type ResumeOptions = { nonce?: NonceOption, signal?: AbortSignal, onError?: (error: mixed) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, }; @@ -141,6 +143,7 @@ function renderToReadableStream( createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, onShellReady, onShellError, @@ -211,6 +214,7 @@ function resume( options ? options.nonce : undefined, ), options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, onShellReady, onShellError, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index d9261dd0f7e..d99dcfef405 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -76,6 +76,7 @@ type Options = { onShellError?: (error: mixed) => void, onAllReady?: () => void, onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, formState?: ReactFormState | null, @@ -89,6 +90,7 @@ type ResumeOptions = { onShellError?: (error: mixed) => void, onAllReady?: () => void, onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, }; type PipeableStream = { @@ -120,6 +122,7 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) { createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, options ? options.onAllReady : undefined, options ? options.onShellReady : undefined, options ? options.onShellError : undefined, @@ -278,6 +281,7 @@ function renderToReadableStream( createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, onShellReady, onShellError, @@ -313,6 +317,7 @@ function resumeRequestImpl( options ? options.nonce : undefined, ), options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, options ? options.onAllReady : undefined, options ? options.onShellReady : undefined, options ? options.onShellError : undefined, @@ -415,6 +420,7 @@ function resume( options ? options.nonce : undefined, ), options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, onShellReady, onShellError, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js index 313b7f9c49f..91c5e59384c 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js @@ -53,6 +53,7 @@ type Options = { progressiveChunkSize?: number, signal?: AbortSignal, onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, onHeaders?: (headers: Headers) => void, @@ -124,6 +125,7 @@ function prerender( createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, undefined, undefined, @@ -149,6 +151,7 @@ type ResumeOptions = { nonce?: NonceOption, signal?: AbortSignal, onError?: (error: mixed) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, }; @@ -189,6 +192,7 @@ function resumeAndPrerender( postponedState, resumeRenderState(postponedState.resumableState, undefined), options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, undefined, undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js index 3196b3f7ce2..0117123523a 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js @@ -53,6 +53,7 @@ type Options = { progressiveChunkSize?: number, signal?: AbortSignal, onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, onHeaders?: (headers: Headers) => void, @@ -123,6 +124,7 @@ function prerender( createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, undefined, undefined, @@ -148,6 +150,7 @@ type ResumeOptions = { nonce?: NonceOption, signal?: AbortSignal, onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, }; function resumeAndPrerender( @@ -187,6 +190,7 @@ function resumeAndPrerender( postponedState, resumeRenderState(postponedState.resumableState, undefined), options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, undefined, undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js index 3f777ae53d9..7aabc76608b 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js @@ -57,6 +57,7 @@ type Options = { progressiveChunkSize?: number, signal?: AbortSignal, onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, onHeaders?: (headers: HeadersDescriptor) => void, @@ -155,6 +156,7 @@ function prerenderToNodeStream( createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, undefined, undefined, @@ -245,6 +247,7 @@ function prerender( createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, undefined, undefined, @@ -270,6 +273,7 @@ type ResumeOptions = { nonce?: NonceOption, signal?: AbortSignal, onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, }; function resumeAndPrerenderToNodeStream( @@ -299,6 +303,7 @@ function resumeAndPrerenderToNodeStream( postponedState, resumeRenderState(postponedState.resumableState, undefined), options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, undefined, undefined, @@ -365,6 +370,7 @@ function resumeAndPrerender( postponedState, resumeRenderState(postponedState.resumableState, undefined), options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, onAllReady, undefined, undefined, diff --git a/packages/react-dom/src/server/ReactDOMLegacyServerImpl.js b/packages/react-dom/src/server/ReactDOMLegacyServerImpl.js index 0dfc0f34d60..ae66f14e1b0 100644 --- a/packages/react-dom/src/server/ReactDOMLegacyServerImpl.js +++ b/packages/react-dom/src/server/ReactDOMLegacyServerImpl.js @@ -72,6 +72,7 @@ function renderToStringImpl( Infinity, onError, undefined, + undefined, onShellReady, undefined, undefined, diff --git a/packages/react-dom/src/shared/ReactDOMBrowser.js b/packages/react-dom/src/shared/ReactDOMBrowser.js index caabe635494..eaf1c05d83d 100644 --- a/packages/react-dom/src/shared/ReactDOMBrowser.js +++ b/packages/react-dom/src/shared/ReactDOMBrowser.js @@ -16,12 +16,7 @@ const browserImpl = function browser(): ReactRecoverable { // Recoverables are Errors so that a renderer can preserve the browser() call // site as the cause if no downstream renderer can recover the subtree. const recoverable = new Error( - "Recoverable Exception: This is not a real error! It's an " + - 'implementation detail of `use(browser())` to defer rendering to the ' + - 'browser. `use(browser())` can only be used inside a `` ' + - 'boundary. If a server render errors with this as its cause, the ' + - 'component that called `use(browser())` does not have a `` ' + - 'boundary above it.', + 'Browser-only rendering was requested by `browser()`.', ); Object.defineProperty(recoverable as any, '$$typeof', { value: REACT_RECOVERABLE_TYPE, diff --git a/packages/react-markup/src/ReactMarkupClient.js b/packages/react-markup/src/ReactMarkupClient.js index e71c41d4415..3c70e21e003 100644 --- a/packages/react-markup/src/ReactMarkupClient.js +++ b/packages/react-markup/src/ReactMarkupClient.js @@ -85,6 +85,7 @@ export function experimental_renderToHTML( undefined, undefined, undefined, + undefined, ); if (options && options.signal) { const signal = options.signal; diff --git a/packages/react-markup/src/ReactMarkupServer.js b/packages/react-markup/src/ReactMarkupServer.js index a885bd38f64..15a2e7827c1 100644 --- a/packages/react-markup/src/ReactMarkupServer.js +++ b/packages/react-markup/src/ReactMarkupServer.js @@ -215,6 +215,7 @@ export function experimental_renderToHTML( undefined, undefined, undefined, + undefined, ); if (options && options.signal) { const signal = options.signal; diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js index d8397eab1ac..04f067a92ba 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -358,6 +358,7 @@ type Options = { onShellReady?: () => void, onAllReady?: () => void, onError?: (error: mixed) => ?string, + onBrowserBailout?: (error: mixed) => void, }; function render(children: React$Element, options?: Options): Destination { @@ -383,6 +384,7 @@ function render(children: React$Element, options?: Options): Destination { null, options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, + options ? options.onBrowserBailout : undefined, options ? options.onAllReady : undefined, options ? options.onShellReady : undefined, ); diff --git a/packages/react-server-dom-fb/src/ReactDOMServerFB.js b/packages/react-server-dom-fb/src/ReactDOMServerFB.js index 1b770988d4b..4bac90d9f2d 100644 --- a/packages/react-server-dom-fb/src/ReactDOMServerFB.js +++ b/packages/react-server-dom-fb/src/ReactDOMServerFB.js @@ -9,7 +9,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; -import type {Request} from 'react-server/src/ReactFizzServer'; +import type {Request, ErrorInfo} from 'react-server/src/ReactFizzServer'; import type {Destination} from 'react-server/src/ReactServerStreamConfig'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; @@ -35,6 +35,7 @@ type Options = { bootstrapModules: Array, progressiveChunkSize?: number, onError: (error: mixed) => void, + onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, }; @@ -68,6 +69,7 @@ function renderToStream(children: ReactNodeList, options: Options): Stream { createRootFormatContext(undefined), options ? options.progressiveChunkSize : undefined, options.onError, + options.onBrowserBailout, undefined, undefined, ); diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 3a58b5688f9..e42ebcbee61 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -107,6 +107,9 @@ let suspendedRecoverableError: Error | null = null; export function createFatalRecoverableError( recoverable: ReactRecoverable, ): Error { + // This is created eagerly when use() encounters the recoverable so its stack + // points to the component call site. It only becomes fatal if no Suspense + // boundary can recover the render. return new Error( 'The server render could not complete because client rendering was ' + "requested outside a Suspense boundary. See this error's cause for " + @@ -314,10 +317,6 @@ export function getSuspendedRecoverableError(): Error { return error; } -export function clearSuspendedRecoverableError(): void { - suspendedRecoverableError = null; -} - export function checkDidRenderIdHook(): boolean { // This should be called immediately after every finishHooks call. // Conceptually, it's part of the return value of finishHooks; it's only a diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 1af0265ea11..6641ced6898 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -138,7 +138,6 @@ import { RecoverableException, createFatalRecoverableError, getSuspendedRecoverableError, - clearSuspendedRecoverableError, } from './ReactFizzHooks'; import {DefaultAsyncDispatcher} from './ReactFizzAsyncDispatcher'; import { @@ -415,6 +414,9 @@ export opaque type Request = { // The return string is used in production primarily to avoid leaking internals, secondarily to save bytes. // Returning null/undefined will cause a default error message in production onError: (error: mixed, errorInfo: ThrownInfo) => ?string, + // onBrowserBailout is called when Fizz recovers by intentionally deferring + // rendering to the browser. + onBrowserBailout: (error: mixed, errorInfo: ThrownInfo) => void, // onAllReady is called when all pending task is done but it may not have flushed yet. // This is a good time to start writing if you want only HTML and no intermediate steps. onAllReady: () => void, @@ -536,6 +538,7 @@ function RequestInstance( rootFormatContext: FormatContext, progressiveChunkSize: void | number, onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string), + onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void), onAllReady: void | (() => void), onShellReady: void | (() => void), onShellError: void | ((error: mixed) => void), @@ -572,6 +575,8 @@ function RequestInstance( this.trackedPostpones = null; this.postponedState = null; this.onError = onError === undefined ? defaultErrorHandler : onError; + this.onBrowserBailout = + onBrowserBailout === undefined ? noop : onBrowserBailout; this.onAllReady = onAllReady === undefined ? noop : onAllReady; this.onShellReady = onShellReady === undefined ? noop : onShellReady; this.onShellError = onShellError === undefined ? noop : onShellError; @@ -589,6 +594,7 @@ export function createRequest( rootFormatContext: FormatContext, progressiveChunkSize: void | number, onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string), + onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void), onAllReady: void | (() => void), onShellReady: void | (() => void), onShellError: void | ((error: mixed) => void), @@ -606,6 +612,7 @@ export function createRequest( rootFormatContext, progressiveChunkSize, onError, + onBrowserBailout, onAllReady, onShellReady, onShellError, @@ -656,6 +663,7 @@ export function createPrerenderRequest( rootFormatContext: FormatContext, progressiveChunkSize: void | number, onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string), + onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void), onAllReady: void | (() => void), onShellReady: void | (() => void), onShellError: void | ((error: mixed) => void), @@ -668,6 +676,7 @@ export function createPrerenderRequest( rootFormatContext, progressiveChunkSize, onError, + onBrowserBailout, onAllReady, onShellReady, onShellError, @@ -688,6 +697,7 @@ export function resumeRequest( postponedState: PostponedState, renderState: RenderState, onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string), + onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void), onAllReady: void | (() => void), onShellReady: void | (() => void), onShellError: void | ((error: mixed) => void), @@ -704,6 +714,7 @@ export function resumeRequest( postponedState.rootFormatContext, postponedState.progressiveChunkSize, onError, + onBrowserBailout, onAllReady, onShellReady, onShellError, @@ -782,6 +793,7 @@ export function resumeAndPrerenderRequest( postponedState: PostponedState, renderState: RenderState, onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string), + onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void), onAllReady: void | (() => void), onShellReady: void | (() => void), onShellError: void | ((error: mixed) => void), @@ -792,6 +804,7 @@ export function resumeAndPrerenderRequest( postponedState, renderState, onError, + onBrowserBailout, onAllReady, onShellReady, onShellError, @@ -1363,7 +1376,16 @@ function logRecoverableError( debugTask: null | ConsoleTask, ): ?string { if (error === RecoverableException) { - clearSuspendedRecoverableError(); + // The fatal wrapper was created eagerly to capture the use() call site, but + // this path recovered at a Suspense boundary. Report its original cause and + // discard the wrapper. + const fatalRecoverableError = getSuspendedRecoverableError(); + logBrowserBailout( + request, + fatalRecoverableError.cause, + errorInfo, + debugTask, + ); return REACT_RECOVERABLE_DIGEST; } @@ -1391,6 +1413,22 @@ function logRecoverableError( return errorDigest === '' ? undefined : errorDigest; } +function logBrowserBailout( + request: Request, + error: mixed, + errorInfo: ThrownInfo, + debugTask: null | ConsoleTask, +): void { + // If this callback errors, we intentionally let that error bubble up to + // become a fatal error, matching the behavior of onError. + const onBrowserBailout = request.onBrowserBailout; + if (__DEV__ && debugTask) { + debugTask.run(onBrowserBailout.bind(null, error, errorInfo)); + } else { + onBrowserBailout(error, errorInfo); + } +} + function fatalError( request: Request, error: mixed, @@ -4861,6 +4899,7 @@ function finishAbortedTask(task: Task, request: Request, error: mixed): void { let errorDigest; let errorForBoundary; if (isRecoverableAbort) { + logBrowserBailout(request, error, errorInfo, null); errorDigest = REACT_RECOVERABLE_DIGEST; errorForBoundary = RecoverableException; } else { @@ -4908,12 +4947,21 @@ function finishAbortedTask(task: Task, request: Request, error: mixed): void { boundary.status = CLIENT_RENDERED; // We are aborting a render or resume which should put boundaries // into an explicitly client rendered state - const errorDigest = isRecoverableAbort - ? REACT_RECOVERABLE_DIGEST - : logRecoverableError(request, error, errorInfo, task.debugTask); - const errorForBoundary = isRecoverableAbort - ? RecoverableException - : error; + let errorDigest; + let errorForBoundary; + if (isRecoverableAbort) { + logBrowserBailout(request, error, errorInfo, task.debugTask); + errorDigest = REACT_RECOVERABLE_DIGEST; + errorForBoundary = RecoverableException; + } else { + errorDigest = logRecoverableError( + request, + error, + errorInfo, + task.debugTask, + ); + errorForBoundary = error; + } encodeErrorForBoundary( boundary, errorDigest, diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index f2eb43edb17..a1ebcc3106f 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -588,7 +588,7 @@ "600": "A rejected Promise was passed to React without a `reason` property. React threw a generic error from where the Promise was used to assist in identifying the problematic Promise. Make sure that instrumented Promises correctly set the `reason` property when setting `status` to `'rejected'`.", "601": "A chunk pair is incomplete. This is a bug in React.", "602": "Cannot handle action key. This is a bug in React.", - "603": "Recoverable Exception: This is not a real error! It's an implementation detail of `use(browser())` to defer rendering to the browser. `use(browser())` can only be used inside a `` boundary. If a server render errors with this as its cause, the component that called `use(browser())` does not have a `` boundary above it.", + "603": "Browser-only rendering was requested by `browser()`.", "604": "The server render could not complete because client rendering was requested outside a Suspense boundary. See this error's cause for additional details.", "605": "Recoverable Exception: This is not a real error! It's an implementation detail of `use` to interrupt the current render so a downstream renderer can recover it. You must either rethrow it immediately, or move the `use` call outside of the `try/catch` block. Capturing without rethrowing will lead to unexpected behavior.", "606": "Expected a suspended recoverable. This is a bug in React. Please file an issue.",