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.",