From db2dc7f9875f00688cd6ffc511abf5849c21952a Mon Sep 17 00:00:00 2001 From: Ricky Date: Thu, 11 Apr 2024 08:58:59 -0400 Subject: [PATCH] Add docs for onCaughtError and onUncaughtError (#6742) * Add docs for onCaughtError and onUncaughtError * Updates from feedback --- src/components/Icon/IconCanary.tsx | 56 +- src/components/MDX/MDXComponents.tsx | 16 + .../reference/react-dom/client/createRoot.md | 817 ++++++++++++++++- .../reference/react-dom/client/hydrateRoot.md | 827 +++++++++++++++++- 4 files changed, 1689 insertions(+), 27 deletions(-) diff --git a/src/components/Icon/IconCanary.tsx b/src/components/Icon/IconCanary.tsx index a7782b14150..7f584fed76e 100644 --- a/src/components/Icon/IconCanary.tsx +++ b/src/components/Icon/IconCanary.tsx @@ -4,29 +4,35 @@ import {memo} from 'react'; -export const IconCanary = memo( - function IconCanary({className, title}) { - return ( - - {title && {title}} - - - - - - - ); +export const IconCanary = memo< + JSX.IntrinsicElements['svg'] & {title?: string; size?: 's' | 'md'} +>(function IconCanary( + {className, title, size} = { + className: undefined, + title: undefined, + size: 'md', } -); +) { + return ( + + {title && {title}} + + + + + + + ); +}); diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx index 76bf86eaa0f..e42b3b2e870 100644 --- a/src/components/MDX/MDXComponents.tsx +++ b/src/components/MDX/MDXComponents.tsx @@ -33,6 +33,7 @@ import type {Toc, TocItem} from './TocContext'; import {TeamMember} from './TeamMember'; import ErrorDecoder from './ErrorDecoder'; +import {IconCanary} from '../Icon/IconCanary'; function CodeStep({children, step}: {children: any; step: number}) { return ( @@ -94,6 +95,20 @@ const Canary = ({children}: {children: React.ReactNode}) => ( {children} ); +const CanaryBadge = ({title}: {title: string}) => ( + + + Canary only + +); + const Blockquote = ({ children, ...props @@ -430,6 +445,7 @@ export const MDXComponents = { MathI, Note, Canary, + CanaryBadge, PackageImport, ReadBlogPost, Recap, diff --git a/src/content/reference/react-dom/client/createRoot.md b/src/content/reference/react-dom/client/createRoot.md index afddb4177d3..b336b6e5ed2 100644 --- a/src/content/reference/react-dom/client/createRoot.md +++ b/src/content/reference/react-dom/client/createRoot.md @@ -45,7 +45,9 @@ An app fully built with React will usually only have one `createRoot` call for i * **optional** `options`: An object with options for this React root. - * **optional** `onRecoverableError`: Callback called when React automatically recovers from errors. + * **optional** `onCaughtError`: Callback called when React catches an error in an Error Boundary. Called with the `error` caught by the Error Boundary, and an `errorInfo` object containing the `componentStack`. + * **optional** `onUncaughtError`: Callback called when an error is thrown and not caught by an Error Boundary. Called with the `error` that was thrown, and an `errorInfo` object containing the `componentStack`. + * **optional** `onRecoverableError`: Callback called when React automatically recovers from errors. Called with an `error` React throws, and an `errorInfo` object containing the `componentStack`. Some recoverable errors may include the original error cause as `error.cause`. * **optional** `identifierPrefix`: A string prefix React uses for IDs generated by [`useId`.](/reference/react/useId) Useful to avoid conflicts when using multiple roots on the same page. #### Returns {/*returns*/} @@ -342,6 +344,797 @@ export default function App({counter}) { It is uncommon to call `render` multiple times. Usually, your components will [update state](/reference/react/useState) instead. +### Show a dialog for uncaught errors {/*show-a-dialog-for-uncaught-errors*/} + + + +`onUncaughtError` is only available in the latest React Canary release. + + + +By default, React will log all uncaught errors to the console. To implement your own error reporting, you can provide the optional `onUncaughtError` root option: + +```js [[1, 6, "onUncaughtError"], [2, 6, "error", 1], [3, 6, "errorInfo"], [4, 10, "componentStack"]] +import { createRoot } from 'react-dom/client'; + +const root = createRoot( + document.getElementById('root'), + { + onUncaughtError: (error, errorInfo) => { + console.error( + 'Uncaught error', + error, + errorInfo.componentStack + ); + } + } +); +root.render(); +``` + +The onUncaughtError option is a function called with two arguments: + +1. The error that was thrown. +2. An errorInfo object that contains the componentStack of the error. + +You can use the `onUncaughtError` root option to display error dialogs: + + + +```html index.html hidden + + + + My app + + + + + +
+ + +``` + +```css src/styles.css active +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } + +#error-dialog { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + padding: 15px; + opacity: 0.9; + text-wrap: wrap; + overflow: scroll; +} + +.text-red { + color: red; +} + +.-mb-20 { + margin-bottom: -20px; +} + +.mb-0 { + margin-bottom: 0; +} + +.mb-10 { + margin-bottom: 10px; +} + +pre { + text-wrap: wrap; +} + +pre.nowrap { + text-wrap: nowrap; +} + +.hidden { + display: none; +} +``` + +```js src/reportError.js hidden +function reportError({ title, error, componentStack, dismissable }) { + const errorDialog = document.getElementById("error-dialog"); + const errorTitle = document.getElementById("error-title"); + const errorMessage = document.getElementById("error-message"); + const errorBody = document.getElementById("error-body"); + const errorComponentStack = document.getElementById("error-component-stack"); + const errorStack = document.getElementById("error-stack"); + const errorClose = document.getElementById("error-close"); + const errorCause = document.getElementById("error-cause"); + const errorCauseMessage = document.getElementById("error-cause-message"); + const errorCauseStack = document.getElementById("error-cause-stack"); + const errorNotDismissible = document.getElementById("error-not-dismissible"); + + // Set the title + errorTitle.innerText = title; + + // Display error message and body + const [heading, body] = error.message.split(/\n(.*)/s); + errorMessage.innerText = heading; + if (body) { + errorBody.innerText = body; + } else { + errorBody.innerText = ''; + } + + // Display component stack + errorComponentStack.innerText = componentStack; + + // Display the call stack + // Since we already displayed the message, strip it, and the first Error: line. + errorStack.innerText = error.stack.replace(error.message, '').split(/\n(.*)/s)[1]; + + // Display the cause, if available + if (error.cause) { + errorCauseMessage.innerText = error.cause.message; + errorCauseStack.innerText = error.cause.stack; + errorCause.classList.remove('hidden'); + } else { + errorCause.classList.add('hidden'); + } + // Display the close button, if dismissible + if (dismissable) { + errorNotDismissible.classList.add('hidden'); + errorClose.classList.remove("hidden"); + } else { + errorNotDismissible.classList.remove('hidden'); + errorClose.classList.add("hidden"); + } + + // Show the dialog + errorDialog.classList.remove("hidden"); +} + +export function reportCaughtError({error, cause, componentStack}) { + reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +} + +export function reportUncaughtError({error, cause, componentStack}) { + reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +} + +export function reportRecoverableError({error, cause, componentStack}) { + reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +} +``` + +```js src/index.js active +import { createRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportUncaughtError} from "./reportError"; +import "./styles.css"; + +const container = document.getElementById("root"); +const root = createRoot(container, { + onUncaughtError: (error, errorInfo) => { + if (error.message !== 'Known error') { + reportUncaughtError({ + error, + componentStack: errorInfo.componentStack + }); + } + } +}); +root.render(); +``` + +```js src/App.js +import { useState } from 'react'; + +export default function App() { + const [throwError, setThrowError] = useState(false); + + if (throwError) { + foo.bar = 'baz'; + } + + return ( +
+ This error shows the error dialog: + +
+ ); +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "^5.0.0" + }, + "main": "/index.js" +} +``` + +
+ + +### Displaying Error Boundary errors {/*displaying-error-boundary-errors*/} + + + +`onCaughtError` is only available in the latest React Canary release. + + + +By default, React will log all errors caught by an Error Boundary to `console.error`. To override this behavior, you can provide the optional `onCaughtError` root option to handle errors caught by an [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary): + +```js [[1, 6, "onCaughtError"], [2, 6, "error", 1], [3, 6, "errorInfo"], [4, 10, "componentStack"]] +import { createRoot } from 'react-dom/client'; + +const root = createRoot( + document.getElementById('root'), + { + onCaughtError: (error, errorInfo) => { + console.error( + 'Caught error', + error, + errorInfo.componentStack + ); + } + } +); +root.render(); +``` + +The onCaughtError option is a function called with two arguments: + +1. The error that was caught by the boundary. +2. An errorInfo object that contains the componentStack of the error. + +You can use the `onCaughtError` root option to display error dialogs or filter known errors from logging: + + + +```html index.html hidden + + + + My app + + + + + +
+ + +``` + +```css src/styles.css active +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } + +#error-dialog { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + padding: 15px; + opacity: 0.9; + text-wrap: wrap; + overflow: scroll; +} + +.text-red { + color: red; +} + +.-mb-20 { + margin-bottom: -20px; +} + +.mb-0 { + margin-bottom: 0; +} + +.mb-10 { + margin-bottom: 10px; +} + +pre { + text-wrap: wrap; +} + +pre.nowrap { + text-wrap: nowrap; +} + +.hidden { + display: none; +} +``` + +```js src/reportError.js hidden +function reportError({ title, error, componentStack, dismissable }) { + const errorDialog = document.getElementById("error-dialog"); + const errorTitle = document.getElementById("error-title"); + const errorMessage = document.getElementById("error-message"); + const errorBody = document.getElementById("error-body"); + const errorComponentStack = document.getElementById("error-component-stack"); + const errorStack = document.getElementById("error-stack"); + const errorClose = document.getElementById("error-close"); + const errorCause = document.getElementById("error-cause"); + const errorCauseMessage = document.getElementById("error-cause-message"); + const errorCauseStack = document.getElementById("error-cause-stack"); + const errorNotDismissible = document.getElementById("error-not-dismissible"); + + // Set the title + errorTitle.innerText = title; + + // Display error message and body + const [heading, body] = error.message.split(/\n(.*)/s); + errorMessage.innerText = heading; + if (body) { + errorBody.innerText = body; + } else { + errorBody.innerText = ''; + } + + // Display component stack + errorComponentStack.innerText = componentStack; + + // Display the call stack + // Since we already displayed the message, strip it, and the first Error: line. + errorStack.innerText = error.stack.replace(error.message, '').split(/\n(.*)/s)[1]; + + // Display the cause, if available + if (error.cause) { + errorCauseMessage.innerText = error.cause.message; + errorCauseStack.innerText = error.cause.stack; + errorCause.classList.remove('hidden'); + } else { + errorCause.classList.add('hidden'); + } + // Display the close button, if dismissible + if (dismissable) { + errorNotDismissible.classList.add('hidden'); + errorClose.classList.remove("hidden"); + } else { + errorNotDismissible.classList.remove('hidden'); + errorClose.classList.add("hidden"); + } + + // Show the dialog + errorDialog.classList.remove("hidden"); +} + +export function reportCaughtError({error, cause, componentStack}) { + reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +} + +export function reportUncaughtError({error, cause, componentStack}) { + reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +} + +export function reportRecoverableError({error, cause, componentStack}) { + reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +} +``` + +```js src/index.js active +import { createRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportCaughtError} from "./reportError"; +import "./styles.css"; + +const container = document.getElementById("root"); +const root = createRoot(container, { + onCaughtError: (error, errorInfo) => { + if (error.message !== 'Known error') { + reportCaughtError({ + error, + componentStack: errorInfo.componentStack, + }); + } + } +}); +root.render(); +``` + +```js src/App.js +import { useState } from 'react'; +import { ErrorBoundary } from "react-error-boundary"; + +export default function App() { + const [error, setError] = useState(null); + + function handleUnknown() { + setError("unknown"); + } + + function handleKnown() { + setError("known"); + } + + return ( + <> + { + setError(null); + }} + > + {error != null && } + This error will not show the error dialog: + + This error will show the error dialog: + + + + + ); +} + +function fallbackRender({ resetErrorBoundary }) { + return ( +
+

Error Boundary

+

Something went wrong.

+ +
+ ); +} + +function Throw({error}) { + if (error === "known") { + throw new Error('Known error') + } else { + foo.bar = 'baz'; + } +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "^5.0.0", + "react-error-boundary": "4.0.3" + }, + "main": "/index.js" +} +``` + +
+ +### Displaying a dialog for recoverable errors {/*displaying-a-dialog-for-recoverable-errors*/} + +React may automatically render a component a second time to attempt to recover from an error thrown in render. If successful, React will log a recoverable error to the console to notify the developer. To override this behavior, you can provide the optional `onRecoverableError` root option: + +```js [[1, 6, "onRecoverableError"], [2, 6, "error", 1], [3, 10, "error.cause"], [4, 6, "errorInfo"], [5, 11, "componentStack"]] +import { createRoot } from 'react-dom/client'; + +const root = createRoot( + document.getElementById('root'), + { + onRecoverableError: (error, errorInfo) => { + console.error( + 'Recoverable error', + error, + error.cause, + errorInfo.componentStack, + ); + } + } +); +root.render(); +``` + +The onRecoverableError option is a function called with two arguments: + +1. The error that React throws. Some errors may include the original cause as error.cause. +2. An errorInfo object that contains the componentStack of the error. + +You can use the `onRecoverableError` root option to display error dialogs: + + + +```html index.html hidden + + + + My app + + + + + +
+ + +``` + +```css src/styles.css active +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } + +#error-dialog { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + padding: 15px; + opacity: 0.9; + text-wrap: wrap; + overflow: scroll; +} + +.text-red { + color: red; +} + +.-mb-20 { + margin-bottom: -20px; +} + +.mb-0 { + margin-bottom: 0; +} + +.mb-10 { + margin-bottom: 10px; +} + +pre { + text-wrap: wrap; +} + +pre.nowrap { + text-wrap: nowrap; +} + +.hidden { + display: none; +} +``` + +```js src/reportError.js hidden +function reportError({ title, error, componentStack, dismissable }) { + const errorDialog = document.getElementById("error-dialog"); + const errorTitle = document.getElementById("error-title"); + const errorMessage = document.getElementById("error-message"); + const errorBody = document.getElementById("error-body"); + const errorComponentStack = document.getElementById("error-component-stack"); + const errorStack = document.getElementById("error-stack"); + const errorClose = document.getElementById("error-close"); + const errorCause = document.getElementById("error-cause"); + const errorCauseMessage = document.getElementById("error-cause-message"); + const errorCauseStack = document.getElementById("error-cause-stack"); + const errorNotDismissible = document.getElementById("error-not-dismissible"); + + // Set the title + errorTitle.innerText = title; + + // Display error message and body + const [heading, body] = error.message.split(/\n(.*)/s); + errorMessage.innerText = heading; + if (body) { + errorBody.innerText = body; + } else { + errorBody.innerText = ''; + } + + // Display component stack + errorComponentStack.innerText = componentStack; + + // Display the call stack + // Since we already displayed the message, strip it, and the first Error: line. + errorStack.innerText = error.stack.replace(error.message, '').split(/\n(.*)/s)[1]; + + // Display the cause, if available + if (error.cause) { + errorCauseMessage.innerText = error.cause.message; + errorCauseStack.innerText = error.cause.stack; + errorCause.classList.remove('hidden'); + } else { + errorCause.classList.add('hidden'); + } + // Display the close button, if dismissible + if (dismissable) { + errorNotDismissible.classList.add('hidden'); + errorClose.classList.remove("hidden"); + } else { + errorNotDismissible.classList.remove('hidden'); + errorClose.classList.add("hidden"); + } + + // Show the dialog + errorDialog.classList.remove("hidden"); +} + +export function reportCaughtError({error, cause, componentStack}) { + reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +} + +export function reportUncaughtError({error, cause, componentStack}) { + reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +} + +export function reportRecoverableError({error, cause, componentStack}) { + reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +} +``` + +```js src/index.js active +import { createRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportRecoverableError} from "./reportError"; +import "./styles.css"; + +const container = document.getElementById("root"); +const root = createRoot(container, { + onRecoverableError: (error, errorInfo) => { + reportRecoverableError({ + error, + cause: error.cause, + componentStack: errorInfo.componentStack, + }); + } +}); +root.render(); +``` + +```js src/App.js +import { useState } from 'react'; +import { ErrorBoundary } from "react-error-boundary"; + +// 🚩 Bug: Never do this. This will force an error. +let errorThrown = false; +export default function App() { + return ( + <> + + {!errorThrown && } +

This component threw an error, but recovered during a second render.

+

Since it recovered, no Error Boundary was shown, but onRecoverableError was used to show an error dialog.

+
+ + + ); +} + +function fallbackRender() { + return ( +
+

Error Boundary

+

Something went wrong.

+
+ ); +} + +function Throw({error}) { + // Simulate an external value changing during concurrent render. + errorThrown = true; + foo.bar = 'baz'; +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "^5.0.0", + "react-error-boundary": "4.0.3" + }, + "main": "/index.js" +} +``` + +
+ + --- ## Troubleshooting {/*troubleshooting*/} @@ -361,6 +1154,28 @@ Until you do that, nothing is displayed. --- +### I'm getting an error: "You passed a second argument to root.render" {/*im-getting-an-error-you-passed-a-second-argument-to-root-render*/} + +A common mistake is to pass the options for `createRoot` to `root.render(...)`: + + + +Warning: You passed a second argument to root.render(...) but it only accepts one argument. + + + +To fix, pass the root options to `createRoot(...)`, not `root.render(...)`: +```js {2,5} +// 🚩 Wrong: root.render only takes one argument. +root.render(App, {onUncaughtError}); + +// ✅ Correct: pass options to createRoot. +const root = createRoot(container, {onUncaughtError}); +root.render(); +``` + +--- + ### I'm getting an error: "Target container is not a DOM element" {/*im-getting-an-error-target-container-is-not-a-dom-element*/} This error means that whatever you're passing to `createRoot` is not a DOM node. diff --git a/src/content/reference/react-dom/client/hydrateRoot.md b/src/content/reference/react-dom/client/hydrateRoot.md index c137cdda9d5..cc30ce22c1f 100644 --- a/src/content/reference/react-dom/client/hydrateRoot.md +++ b/src/content/reference/react-dom/client/hydrateRoot.md @@ -41,7 +41,9 @@ React will attach to the HTML that exists inside the `domNode`, and take over ma * **optional** `options`: An object with options for this React root. - * **optional** `onRecoverableError`: Callback called when React automatically recovers from errors. + * **optional** `onCaughtError`: Callback called when React catches an error in an Error Boundary. Called with the `error` caught by the Error Boundary, and an `errorInfo` object containing the `componentStack`. + * **optional** `onUncaughtError`: Callback called when an error is thrown and not caught by an Error Boundary. Called with the `error` that was thrown and an `errorInfo` object containing the `componentStack`. + * **optional** `onRecoverableError`: Callback called when React automatically recovers from errors. Called with the `error` React throws, and an `errorInfo` object containing the `componentStack`. Some recoverable errors may include the original error cause as `error.cause`. * **optional** `identifierPrefix`: A string prefix React uses for IDs generated by [`useId`.](/reference/react/useId) Useful to avoid conflicts when using multiple roots on the same page. Must be the same prefix as used on the server. @@ -371,3 +373,826 @@ export default function App({counter}) { It is uncommon to call [`root.render`](#root-render) on a hydrated root. Usually, you'll [update state](/reference/react/useState) inside one of the components instead. + +### Show a dialog for uncaught errors {/*show-a-dialog-for-uncaught-errors*/} + + + +`onUncaughtError` is only available in the latest React Canary release. + + + +By default, React will log all uncaught errors to the console. To implement your own error reporting, you can provide the optional `onUncaughtError` root option: + +```js [[1, 7, "onUncaughtError"], [2, 7, "error", 1], [3, 7, "errorInfo"], [4, 11, "componentStack"]] +import { hydrateRoot } from 'react-dom/client'; + +const root = hydrateRoot( + document.getElementById('root'), + , + { + onUncaughtError: (error, errorInfo) => { + console.error( + 'Uncaught error', + error, + errorInfo.componentStack + ); + } + } +); +root.render(); +``` + +The onUncaughtError option is a function called with two arguments: + +1. The error that was thrown. +2. An errorInfo object that contains the componentStack of the error. + +You can use the `onUncaughtError` root option to display error dialogs: + + + +```html index.html hidden + + + + My app + + + + + +
This error shows the error dialog:
+ + +``` + +```css src/styles.css active +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } + +#error-dialog { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + padding: 15px; + opacity: 0.9; + text-wrap: wrap; + overflow: scroll; +} + +.text-red { + color: red; +} + +.-mb-20 { + margin-bottom: -20px; +} + +.mb-0 { + margin-bottom: 0; +} + +.mb-10 { + margin-bottom: 10px; +} + +pre { + text-wrap: wrap; +} + +pre.nowrap { + text-wrap: nowrap; +} + +.hidden { + display: none; +} +``` + +```js src/reportError.js hidden +function reportError({ title, error, componentStack, dismissable }) { + const errorDialog = document.getElementById("error-dialog"); + const errorTitle = document.getElementById("error-title"); + const errorMessage = document.getElementById("error-message"); + const errorBody = document.getElementById("error-body"); + const errorComponentStack = document.getElementById("error-component-stack"); + const errorStack = document.getElementById("error-stack"); + const errorClose = document.getElementById("error-close"); + const errorCause = document.getElementById("error-cause"); + const errorCauseMessage = document.getElementById("error-cause-message"); + const errorCauseStack = document.getElementById("error-cause-stack"); + const errorNotDismissible = document.getElementById("error-not-dismissible"); + + // Set the title + errorTitle.innerText = title; + + // Display error message and body + const [heading, body] = error.message.split(/\n(.*)/s); + errorMessage.innerText = heading; + if (body) { + errorBody.innerText = body; + } else { + errorBody.innerText = ''; + } + + // Display component stack + errorComponentStack.innerText = componentStack; + + // Display the call stack + // Since we already displayed the message, strip it, and the first Error: line. + errorStack.innerText = error.stack.replace(error.message, '').split(/\n(.*)/s)[1]; + + // Display the cause, if available + if (error.cause) { + errorCauseMessage.innerText = error.cause.message; + errorCauseStack.innerText = error.cause.stack; + errorCause.classList.remove('hidden'); + } else { + errorCause.classList.add('hidden'); + } + // Display the close button, if dismissible + if (dismissable) { + errorNotDismissible.classList.add('hidden'); + errorClose.classList.remove("hidden"); + } else { + errorNotDismissible.classList.remove('hidden'); + errorClose.classList.add("hidden"); + } + + // Show the dialog + errorDialog.classList.remove("hidden"); +} + +export function reportCaughtError({error, cause, componentStack}) { + reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +} + +export function reportUncaughtError({error, cause, componentStack}) { + reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +} + +export function reportRecoverableError({error, cause, componentStack}) { + reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +} +``` + +```js src/index.js active +import { hydrateRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportUncaughtError} from "./reportError"; +import "./styles.css"; +import {renderToString} from 'react-dom/server'; + +const container = document.getElementById("root"); +const root = hydrateRoot(container, , { + onUncaughtError: (error, errorInfo) => { + if (error.message !== 'Known error') { + reportUncaughtError({ + error, + componentStack: errorInfo.componentStack + }); + } + } +}); +``` + +```js src/App.js +import { useState } from 'react'; + +export default function App() { + const [throwError, setThrowError] = useState(false); + + if (throwError) { + foo.bar = 'baz'; + } + + return ( +
+ This error shows the error dialog: + +
+ ); +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "^5.0.0" + }, + "main": "/index.js" +} +``` + +
+ + +### Displaying Error Boundary errors {/*displaying-error-boundary-errors*/} + + + +`onCaughtError` is only available in the latest React Canary release. + + + +By default, React will log all errors caught by an Error Boundary to `console.error`. To override this behavior, you can provide the optional `onCaughtError` root option for errors caught by an [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary): + +```js [[1, 7, "onCaughtError"], [2, 7, "error", 1], [3, 7, "errorInfo"], [4, 11, "componentStack"]] +import { hydrateRoot } from 'react-dom/client'; + +const root = hydrateRoot( + document.getElementById('root'), + , + { + onCaughtError: (error, errorInfo) => { + console.error( + 'Caught error', + error, + errorInfo.componentStack + ); + } + } +); +root.render(); +``` + +The onCaughtError option is a function called with two arguments: + +1. The error that was caught by the boundary. +2. An errorInfo object that contains the componentStack of the error. + +You can use the `onCaughtError` root option to display error dialogs or filter known errors from logging: + + + +```html index.html hidden + + + + My app + + + + + +
This error will not show the error dialog:This error will show the error dialog:
+ + +``` + +```css src/styles.css active +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } + +#error-dialog { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + padding: 15px; + opacity: 0.9; + text-wrap: wrap; + overflow: scroll; +} + +.text-red { + color: red; +} + +.-mb-20 { + margin-bottom: -20px; +} + +.mb-0 { + margin-bottom: 0; +} + +.mb-10 { + margin-bottom: 10px; +} + +pre { + text-wrap: wrap; +} + +pre.nowrap { + text-wrap: nowrap; +} + +.hidden { + display: none; +} +``` + +```js src/reportError.js hidden +function reportError({ title, error, componentStack, dismissable }) { + const errorDialog = document.getElementById("error-dialog"); + const errorTitle = document.getElementById("error-title"); + const errorMessage = document.getElementById("error-message"); + const errorBody = document.getElementById("error-body"); + const errorComponentStack = document.getElementById("error-component-stack"); + const errorStack = document.getElementById("error-stack"); + const errorClose = document.getElementById("error-close"); + const errorCause = document.getElementById("error-cause"); + const errorCauseMessage = document.getElementById("error-cause-message"); + const errorCauseStack = document.getElementById("error-cause-stack"); + const errorNotDismissible = document.getElementById("error-not-dismissible"); + + // Set the title + errorTitle.innerText = title; + + // Display error message and body + const [heading, body] = error.message.split(/\n(.*)/s); + errorMessage.innerText = heading; + if (body) { + errorBody.innerText = body; + } else { + errorBody.innerText = ''; + } + + // Display component stack + errorComponentStack.innerText = componentStack; + + // Display the call stack + // Since we already displayed the message, strip it, and the first Error: line. + errorStack.innerText = error.stack.replace(error.message, '').split(/\n(.*)/s)[1]; + + // Display the cause, if available + if (error.cause) { + errorCauseMessage.innerText = error.cause.message; + errorCauseStack.innerText = error.cause.stack; + errorCause.classList.remove('hidden'); + } else { + errorCause.classList.add('hidden'); + } + // Display the close button, if dismissible + if (dismissable) { + errorNotDismissible.classList.add('hidden'); + errorClose.classList.remove("hidden"); + } else { + errorNotDismissible.classList.remove('hidden'); + errorClose.classList.add("hidden"); + } + + // Show the dialog + errorDialog.classList.remove("hidden"); +} + +export function reportCaughtError({error, cause, componentStack}) { + reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +} + +export function reportUncaughtError({error, cause, componentStack}) { + reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +} + +export function reportRecoverableError({error, cause, componentStack}) { + reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +} +``` + +```js src/index.js active +import { hydrateRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportCaughtError} from "./reportError"; +import "./styles.css"; + +const container = document.getElementById("root"); +const root = hydrateRoot(container, , { + onCaughtError: (error, errorInfo) => { + if (error.message !== 'Known error') { + reportCaughtError({ + error, + componentStack: errorInfo.componentStack + }); + } + } +}); +``` + +```js src/App.js +import { useState } from 'react'; +import { ErrorBoundary } from "react-error-boundary"; + +export default function App() { + const [error, setError] = useState(null); + + function handleUnknown() { + setError("unknown"); + } + + function handleKnown() { + setError("known"); + } + + return ( + <> + { + setError(null); + }} + > + {error != null && } + This error will not show the error dialog: + + This error will show the error dialog: + + + + + ); +} + +function fallbackRender({ resetErrorBoundary }) { + return ( +
+

Error Boundary

+

Something went wrong.

+ +
+ ); +} + +function Throw({error}) { + if (error === "known") { + throw new Error('Known error') + } else { + foo.bar = 'baz'; + } +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "^5.0.0", + "react-error-boundary": "4.0.3" + }, + "main": "/index.js" +} +``` + +
+ +### Show a dialog for recoverable hydration mismatch errors {/*show-a-dialog-for-recoverable-hydration-mismatch-errors*/} + +When React encounters a hydration mismatch, it will automatically attempt to recover by rendering on the client. By default, React will log hydration mismatch errors to `console.error`. To override this behavior, you can provide the optional `onRecoverableError` root option: + +```js [[1, 7, "onRecoverableError"], [2, 7, "error", 1], [3, 11, "error.cause", 1], [4, 7, "errorInfo"], [5, 12, "componentStack"]] +import { hydrateRoot } from 'react-dom/client'; + +const root = hydrateRoot( + document.getElementById('root'), + , + { + onRecoverableError: (error, errorInfo) => { + console.error( + 'Caught error', + error, + error.cause, + errorInfo.componentStack + ); + } + } +); +``` + +The onRecoverableError option is a function called with two arguments: + +1. The error React throws. Some errors may include the original cause as error.cause. +2. An errorInfo object that contains the componentStack of the error. + +You can use the `onRecoverableError` root option to display error dialogs for hydration mismatches: + + + +```html index.html hidden + + + + My app + + + + + +
Server
+ + +``` + +```css src/styles.css active +label, button { display: block; margin-bottom: 20px; } +html, body { min-height: 300px; } + +#error-dialog { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + padding: 15px; + opacity: 0.9; + text-wrap: wrap; + overflow: scroll; +} + +.text-red { + color: red; +} + +.-mb-20 { + margin-bottom: -20px; +} + +.mb-0 { + margin-bottom: 0; +} + +.mb-10 { + margin-bottom: 10px; +} + +pre { + text-wrap: wrap; +} + +pre.nowrap { + text-wrap: nowrap; +} + +.hidden { + display: none; +} +``` + +```js src/reportError.js hidden +function reportError({ title, error, componentStack, dismissable }) { + const errorDialog = document.getElementById("error-dialog"); + const errorTitle = document.getElementById("error-title"); + const errorMessage = document.getElementById("error-message"); + const errorBody = document.getElementById("error-body"); + const errorComponentStack = document.getElementById("error-component-stack"); + const errorStack = document.getElementById("error-stack"); + const errorClose = document.getElementById("error-close"); + const errorCause = document.getElementById("error-cause"); + const errorCauseMessage = document.getElementById("error-cause-message"); + const errorCauseStack = document.getElementById("error-cause-stack"); + const errorNotDismissible = document.getElementById("error-not-dismissible"); + + // Set the title + errorTitle.innerText = title; + + // Display error message and body + const [heading, body] = error.message.split(/\n(.*)/s); + errorMessage.innerText = heading; + if (body) { + errorBody.innerText = body; + } else { + errorBody.innerText = ''; + } + + // Display component stack + errorComponentStack.innerText = componentStack; + + // Display the call stack + // Since we already displayed the message, strip it, and the first Error: line. + errorStack.innerText = error.stack.replace(error.message, '').split(/\n(.*)/s)[1]; + + // Display the cause, if available + if (error.cause) { + errorCauseMessage.innerText = error.cause.message; + errorCauseStack.innerText = error.cause.stack; + errorCause.classList.remove('hidden'); + } else { + errorCause.classList.add('hidden'); + } + // Display the close button, if dismissible + if (dismissable) { + errorNotDismissible.classList.add('hidden'); + errorClose.classList.remove("hidden"); + } else { + errorNotDismissible.classList.remove('hidden'); + errorClose.classList.add("hidden"); + } + + // Show the dialog + errorDialog.classList.remove("hidden"); +} + +export function reportCaughtError({error, cause, componentStack}) { + reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +} + +export function reportUncaughtError({error, cause, componentStack}) { + reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +} + +export function reportRecoverableError({error, cause, componentStack}) { + reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +} +``` + +```js src/index.js active +import { hydrateRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportRecoverableError} from "./reportError"; +import "./styles.css"; + +const container = document.getElementById("root"); +const root = hydrateRoot(container, , { + onRecoverableError: (error, errorInfo) => { + reportRecoverableError({ + error, + cause: error.cause, + componentStack: errorInfo.componentStack + }); + } +}); +``` + +```js src/App.js +import { useState } from 'react'; +import { ErrorBoundary } from "react-error-boundary"; + +export default function App() { + const [error, setError] = useState(null); + + function handleUnknown() { + setError("unknown"); + } + + function handleKnown() { + setError("known"); + } + + return ( + {typeof window !== 'undefined' ? 'Client' : 'Server'} + ); +} + +function fallbackRender({ resetErrorBoundary }) { + return ( +
+

Error Boundary

+

Something went wrong.

+ +
+ ); +} + +function Throw({error}) { + if (error === "known") { + throw new Error('Known error') + } else { + foo.bar = 'baz'; + } +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "^5.0.0", + "react-error-boundary": "4.0.3" + }, + "main": "/index.js" +} +``` + +
+ +## Troubleshooting {/*troubleshooting*/} + + +### I'm getting an error: "You passed a second argument to root.render" {/*im-getting-an-error-you-passed-a-second-argument-to-root-render*/} + +A common mistake is to pass the options for `hydrateRoot` to `root.render(...)`: + + + +Warning: You passed a second argument to root.render(...) but it only accepts one argument. + + + +To fix, pass the root options to `hydrateRoot(...)`, not `root.render(...)`: +```js {2,5} +// 🚩 Wrong: root.render only takes one argument. +root.render(App, {onUncaughtError}); + +// ✅ Correct: pass options to createRoot. +const root = hydrateRoot(container, , {onUncaughtError}); +```