From c1948dc6e7e26c761c1b5478b7137521966b0ef3 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Mon, 13 Jan 2025 14:32:17 +0100 Subject: [PATCH 01/20] Reference docs for `captureOwnerStack` --- .../reference/react/captureOwnerStack.md | 180 ++++++++++++++++++ src/sidebarReference.json | 8 +- 2 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 src/content/reference/react/captureOwnerStack.md diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md new file mode 100644 index 00000000000..96b8c25d928 --- /dev/null +++ b/src/content/reference/react/captureOwnerStack.md @@ -0,0 +1,180 @@ +--- +title: captureOwnerStack +--- + + + +**This API is experimental and is not available in a stable version of React yet.** + +You can try it by upgrading React packages to the most recent experimental version: + +- `react@experimental` + +Experimental versions of React may contain bugs. Don't use them in production. + + + + + +`captureOwnerStack` reads the current **owner** Component stack and returns it as a string. +If no owner stack is available, it returns an empty string. + +```js +captureOwnerStack(); +``` + + + + + +--- + +## Reference {/*reference*/} + +### `captureOwnerStack()` {/*captureownerstack*/} + +Call `captureOwnerStack` to get the current owner Component stack. + +```js +import * as React from 'react'; + +function Component() { + if (process.env.NODE_ENV !== 'production') { + const ownerStack = React.captureOwnerStack(); + console.log(ownerStack); + } +} +``` + +#### Parameters {/*parameters*/} + +`captureOwnerStack` does not take any parameters. + +#### Returns {/*returns*/} + +`captureOwnerStack` returns `string`. + +#### Caveats {/*caveats*/} + +`captureOwnerStack` is only available in development builds of React. +In production builds, the `captureOwnerStack` export does not exist. + +Only call `captureOwnerStack` in development environments: + +```tsx +// Use a namespace import to avoid errors in production when using ES modules. +// Non-existing exports throw a `SyntaxError` in ES modules. +import * as React from 'react'; + +let ownerStack = ''; +if (process.env.NODE_ENV !== 'prodction') { + ownerStack = React.captureOwnerStack(); +} +``` + +## Owner Component stacks vs parent Component stacks {/*owner-component-stacks-vs-parent-component-stacks*/} + +The owner Component stack is different from the **parent** Component stack available error handlers like [`errorInfo.componentStack` in `onUncaughtError`](http://localhost:3000/reference/react-dom/client/hydrateRoot#show-a-dialog-for-uncaught-errors). + +For example, consider the following code: + +```tsx +import * as React from 'react'; +import * as ReactDOMClient from 'react-dom/client'; + +function SubComponent({disabled}) { + if (disabled) { + throw new Error('disabled'); + } +} + +function Component({label}) { + return ( +
+ {label} + +
+ ); +} + +function Navigation() { + return null; +} + +function App({children}) { + return ( + +
+ + {children} +
+
+ ); +} + +createRoot(document.createElement('div')).render( + + + +); +``` + +`SubComponent` would throw an error. +The **parent** component stack of that error would be + +``` +at SubComponent +at fieldset +at Component +at main +at React.Suspense +at App +``` + +However, the owner stack would only read + +``` +at SubComponent +at Component +``` + +Neither `App` nor the DOM components (e.g. `fieldset`) are considered owners in this stack since they didn't contribute to "creating" the node containing `SubComponent`. `App` and DOM components only forwarded the node. `App` just rendered the `children` node as opposed to `Component` which created a node containing `SubComponent` via ``. + +Neither `Navigation` nor `legend` are in the stack at all since it's only a sibling to a node containing ``. + +### When to use which {/*when-to-use-which*/} + +The parent stack is useful for contextual information e.g. [`React.Suspense`](/reference/react/Suspense), [React Context](https://react.dev/reference/react/createContext), or [`
`](/reference/react-dom/components/form). + +The owner stack is useful for tracing the flow of props. Owner stacks are equivalent to [call stacks](https://developer.mozilla.org/en-US/docs/Glossary/Call_stack) if all JSX would be converted to direct function calls e.g. `Component({label: "disabled"})` instead of ``. + +## Usage {/*usage*/} + +### Expanding error stacks {/*expanding-error-stacks*/} + +In addition to the stack trace of the error itself, you can use `captureOwnerStack` to append the stack trace of the owner Component. +This can help trace the error especially when the error is caused by props. The owner Component stack helps trace the flow of props. + +```jsx [[9, 15, "error"], [34, 10, "captureOwnerStack"]] +import * as React from 'react' +import { hydrateRoot } from 'react-dom/client'; + +const root = hydrateRoot( + document.getElementById('root'), + , + { + onCaughtError: (error, errorInfo) => { + if (process.env.NODE_ENV !== 'production') { + const ownerStack = React.captureOwnerStack(); + error.stack += ownerStack; + } + console.error( + 'Caught error', + error, + errorInfo.componentStack + ); + } + } +); +root.render(); +``` diff --git a/src/sidebarReference.json b/src/sidebarReference.json index 00917ad56cf..a044c9f5baf 100644 --- a/src/sidebarReference.json +++ b/src/sidebarReference.json @@ -147,6 +147,11 @@ "title": "experimental_taintUniqueValue", "path": "/reference/react/experimental_taintUniqueValue", "version": "canary" + }, + { + "title": "captureOwnerStack", + "path": "/reference/react/captureOwnerStack", + "version": "canary" } ] }, @@ -304,7 +309,6 @@ "title": "prerenderToNodeStream", "path": "/reference/react-dom/static/prerenderToNodeStream" } - ] }, { @@ -398,4 +402,4 @@ ] } ] -} \ No newline at end of file +} From 46525991ab37939a84ab8591f088b2c7a5039bed Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Mon, 13 Jan 2025 14:34:12 +0100 Subject: [PATCH 02/20] Differentiate parent and owner stacks throughout docs --- src/content/reference/react-dom/client/createRoot.md | 10 +++++----- .../reference/react-dom/client/hydrateRoot.md | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/content/reference/react-dom/client/createRoot.md b/src/content/reference/react-dom/client/createRoot.md index 54e7a7f1d8d..ed8ad9db63c 100644 --- a/src/content/reference/react-dom/client/createRoot.md +++ b/src/content/reference/react-dom/client/createRoot.md @@ -45,9 +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** `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** `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 parent Component stack in `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 parent Component stack in `componentStack`. + * **optional** `onRecoverableError`: Callback called when React automatically recovers from errors. Called with an `error` React throws, and an `errorInfo` object containing the parent Component stack in `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*/} @@ -369,7 +369,7 @@ 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. +2. An errorInfo object that contains the parent Component stack in componentStack of the error. You can use the `onUncaughtError` root option to display error dialogs: @@ -600,7 +600,7 @@ 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. +2. An errorInfo object that contains the parent Component stack in componentStack of the error. You can use the `onCaughtError` root option to display error dialogs or filter known errors from logging: diff --git a/src/content/reference/react-dom/client/hydrateRoot.md b/src/content/reference/react-dom/client/hydrateRoot.md index b1eeca30ce6..19208252400 100644 --- a/src/content/reference/react-dom/client/hydrateRoot.md +++ b/src/content/reference/react-dom/client/hydrateRoot.md @@ -41,9 +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** `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** `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 parent Component stack in `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 parent Component stack in `componentStack`. + * **optional** `onRecoverableError`: Callback called when React automatically recovers from errors. Called with the `error` React throws, and an `errorInfo` object containing the parent Component stack in `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. @@ -400,7 +400,7 @@ 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. +2. An errorInfo object that contains the parent Component stack in componentStack of the error. You can use the `onUncaughtError` root option to display error dialogs: @@ -635,7 +635,7 @@ 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. +2. An errorInfo object that contains the parent Component stack in componentStack of the error. You can use the `onCaughtError` root option to display error dialogs or filter known errors from logging: @@ -916,7 +916,7 @@ const root = hydrateRoot( 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. +2. An errorInfo object that contains the parent Component stack in componentStack of the error. You can use the `onRecoverableError` root option to display error dialogs for hydration mismatches: From e054c4fd388e7348ca8e8d65c74e42a9568425e9 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Mon, 13 Jan 2025 16:27:29 +0100 Subject: [PATCH 03/20] null outside of dev --- .../reference/react/captureOwnerStack.md | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index 96b8c25d928..17f4380d027 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -16,8 +16,7 @@ Experimental versions of React may contain bugs. Don't use them in production. -`captureOwnerStack` reads the current **owner** Component stack and returns it as a string. -If no owner stack is available, it returns an empty string. +`captureOwnerStack` reads the current **owner** Component stack and returns it as a string if available. ```js captureOwnerStack(); @@ -52,25 +51,10 @@ function Component() { #### Returns {/*returns*/} -`captureOwnerStack` returns `string`. - -#### Caveats {/*caveats*/} - -`captureOwnerStack` is only available in development builds of React. -In production builds, the `captureOwnerStack` export does not exist. - -Only call `captureOwnerStack` in development environments: - -```tsx -// Use a namespace import to avoid errors in production when using ES modules. -// Non-existing exports throw a `SyntaxError` in ES modules. -import * as React from 'react'; +`captureOwnerStack` returns `string | null`. -let ownerStack = ''; -if (process.env.NODE_ENV !== 'prodction') { - ownerStack = React.captureOwnerStack(); -} -``` +If no owner stack is available, it returns an empty string. +Outside of development builds, `null` is returned. ## Owner Component stacks vs parent Component stacks {/*owner-component-stacks-vs-parent-component-stacks*/} @@ -156,7 +140,7 @@ In addition to the stack trace of the error itself This can help trace the error especially when the error is caused by props. The owner Component stack helps trace the flow of props. ```jsx [[9, 15, "error"], [34, 10, "captureOwnerStack"]] -import * as React from 'react' +import { captureOwnerStack } from 'react' import { hydrateRoot } from 'react-dom/client'; const root = hydrateRoot( @@ -165,7 +149,7 @@ const root = hydrateRoot( { onCaughtError: (error, errorInfo) => { if (process.env.NODE_ENV !== 'production') { - const ownerStack = React.captureOwnerStack(); + const ownerStack = captureOwnerStack(); error.stack += ownerStack; } console.error( From cb8ac7d455539d5550d1097d8ded184d36815254 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Tue, 14 Jan 2025 10:00:30 +0100 Subject: [PATCH 04/20] Update src/content/reference/react/captureOwnerStack.md Co-authored-by: Ricky --- src/content/reference/react/captureOwnerStack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index 17f4380d027..e15825bdc12 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -19,7 +19,7 @@ Experimental versions of React may contain bugs. Don't use them in production. `captureOwnerStack` reads the current **owner** Component stack and returns it as a string if available. ```js -captureOwnerStack(); +const stack = captureOwnerStack(); ``` From 0f33f7afdbfaa6dcb43bedc9f029aa4cc9313dc6 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Tue, 14 Jan 2025 10:07:12 +0100 Subject: [PATCH 05/20] New terminology parent Component stack -> Component Stack owner Component stack -> Owner Stack --- .../reference/react-dom/client/createRoot.md | 10 ++++---- .../reference/react-dom/client/hydrateRoot.md | 12 +++++----- .../reference/react/captureOwnerStack.md | 24 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/content/reference/react-dom/client/createRoot.md b/src/content/reference/react-dom/client/createRoot.md index ed8ad9db63c..54e7a7f1d8d 100644 --- a/src/content/reference/react-dom/client/createRoot.md +++ b/src/content/reference/react-dom/client/createRoot.md @@ -45,9 +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** `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 parent Component stack in `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 parent Component stack in `componentStack`. - * **optional** `onRecoverableError`: Callback called when React automatically recovers from errors. Called with an `error` React throws, and an `errorInfo` object containing the parent Component stack in `componentStack`. Some recoverable errors may include the original error cause as `error.cause`. + * **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*/} @@ -369,7 +369,7 @@ 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 parent Component stack in componentStack of the error. +2. An errorInfo object that contains the componentStack of the error. You can use the `onUncaughtError` root option to display error dialogs: @@ -600,7 +600,7 @@ 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 parent Component stack in componentStack of the error. +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: diff --git a/src/content/reference/react-dom/client/hydrateRoot.md b/src/content/reference/react-dom/client/hydrateRoot.md index 19208252400..b1eeca30ce6 100644 --- a/src/content/reference/react-dom/client/hydrateRoot.md +++ b/src/content/reference/react-dom/client/hydrateRoot.md @@ -41,9 +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** `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 parent Component stack in `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 parent Component stack in `componentStack`. - * **optional** `onRecoverableError`: Callback called when React automatically recovers from errors. Called with the `error` React throws, and an `errorInfo` object containing the parent Component stack in `componentStack`. Some recoverable errors may include the original error cause as `error.cause`. + * **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. @@ -400,7 +400,7 @@ 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 parent Component stack in componentStack of the error. +2. An errorInfo object that contains the componentStack of the error. You can use the `onUncaughtError` root option to display error dialogs: @@ -635,7 +635,7 @@ 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 parent Component stack in componentStack of the error. +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: @@ -916,7 +916,7 @@ const root = hydrateRoot( 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 parent Component stack in componentStack of the error. +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: diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index e15825bdc12..dc324f8ce22 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -16,7 +16,7 @@ Experimental versions of React may contain bugs. Don't use them in production. -`captureOwnerStack` reads the current **owner** Component stack and returns it as a string if available. +`captureOwnerStack` reads the current Owner Stack and returns it as a string if available. ```js const stack = captureOwnerStack(); @@ -32,7 +32,7 @@ const stack = captureOwnerStack(); ### `captureOwnerStack()` {/*captureownerstack*/} -Call `captureOwnerStack` to get the current owner Component stack. +Call `captureOwnerStack` to get the current Owner Stack. ```js import * as React from 'react'; @@ -53,12 +53,12 @@ function Component() { `captureOwnerStack` returns `string | null`. -If no owner stack is available, it returns an empty string. +If no Owner Stack is available, it returns an empty string. Outside of development builds, `null` is returned. -## Owner Component stacks vs parent Component stacks {/*owner-component-stacks-vs-parent-component-stacks*/} +## Owner Stack vs Component Stack {/*owner-stack-vs-component-stack*/} -The owner Component stack is different from the **parent** Component stack available error handlers like [`errorInfo.componentStack` in `onUncaughtError`](http://localhost:3000/reference/react-dom/client/hydrateRoot#show-a-dialog-for-uncaught-errors). +The Owner Stack is different from the Component Stack available error handlers like [`errorInfo.componentStack` in `onUncaughtError`](/reference/react-dom/client/hydrateRoot#show-a-dialog-for-uncaught-errors). For example, consider the following code: @@ -104,7 +104,7 @@ createRoot(document.createElement('div')).render( ``` `SubComponent` would throw an error. -The **parent** component stack of that error would be +The Component Stack of that error would be ``` at SubComponent @@ -115,29 +115,29 @@ at React.Suspense at App ``` -However, the owner stack would only read +However, the Owner Stack would only read ``` at SubComponent at Component ``` -Neither `App` nor the DOM components (e.g. `fieldset`) are considered owners in this stack since they didn't contribute to "creating" the node containing `SubComponent`. `App` and DOM components only forwarded the node. `App` just rendered the `children` node as opposed to `Component` which created a node containing `SubComponent` via ``. +Neither `App` nor the DOM components (e.g. `fieldset`) are considered Owners in this Stack since they didn't contribute to "creating" the node containing `SubComponent`. `App` and DOM components only forwarded the node. `App` just rendered the `children` node as opposed to `Component` which created a node containing `SubComponent` via ``. Neither `Navigation` nor `legend` are in the stack at all since it's only a sibling to a node containing ``. ### When to use which {/*when-to-use-which*/} -The parent stack is useful for contextual information e.g. [`React.Suspense`](/reference/react/Suspense), [React Context](https://react.dev/reference/react/createContext), or [``](/reference/react-dom/components/form). +The Component Stack is useful for contextual information e.g. [`React.Suspense`](/reference/react/Suspense), [React Context](https://react.dev/reference/react/createContext), or [``](/reference/react-dom/components/form). -The owner stack is useful for tracing the flow of props. Owner stacks are equivalent to [call stacks](https://developer.mozilla.org/en-US/docs/Glossary/Call_stack) if all JSX would be converted to direct function calls e.g. `Component({label: "disabled"})` instead of ``. +The Owner Stack is useful for tracing the flow of props. Owner stacks are equivalent to [call stacks](https://developer.mozilla.org/en-US/docs/Glossary/Call_stack) if all JSX would be converted to direct function calls e.g. `Component({label: "disabled"})` instead of ``. ## Usage {/*usage*/} ### Expanding error stacks {/*expanding-error-stacks*/} -In addition to the stack trace of the error itself, you can use `captureOwnerStack` to append the stack trace of the owner Component. -This can help trace the error especially when the error is caused by props. The owner Component stack helps trace the flow of props. +In addition to the stack trace of the error itself, you can use `captureOwnerStack` to append the Owner Stack. +This can help trace the error especially when the error is caused by props. The Owner Stack helps trace the flow of props. ```jsx [[9, 15, "error"], [34, 10, "captureOwnerStack"]] import { captureOwnerStack } from 'react' From 9a882e1328e2a4350c3b65195df4fef274959a2d Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Tue, 14 Jan 2025 10:53:00 +0100 Subject: [PATCH 06/20] Emphasize dev-only nature --- .../reference/react/captureOwnerStack.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index dc324f8ce22..406cd01cd0b 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -16,7 +16,7 @@ Experimental versions of React may contain bugs. Don't use them in production. -`captureOwnerStack` reads the current Owner Stack and returns it as a string if available. +`captureOwnerStack` reads the current Owner Stack in development and returns it as a string if available. ```js const stack = captureOwnerStack(); @@ -56,6 +56,10 @@ function Component() { If no Owner Stack is available, it returns an empty string. Outside of development builds, `null` is returned. +#### Caveats {/*caveats*/} + +- Owner Stacks are only available in development. `captureOwnerStack` will always return `null` outside of development. + ## Owner Stack vs Component Stack {/*owner-stack-vs-component-stack*/} The Owner Stack is different from the Component Stack available error handlers like [`errorInfo.componentStack` in `onUncaughtError`](/reference/react-dom/client/hydrateRoot#show-a-dialog-for-uncaught-errors). @@ -162,3 +166,15 @@ const root = hydrateRoot( ); root.render(); ``` + +## Troubleshooting {/*troubleshooting*/} + +### The Owner Stack is `null` {/*the-owner-stack-is-null*/} + +`captureOwnerStack` was called outside of development builds. +For performance reasons, React will not keep track of Owners in production. + +### The Owner Stack is empty {/*the-owner-stack-is-empty*/} + +The call of `captureOwnerStack` happened outside of a React controlled function e.g. in a `setTimeout` callback. +During render, Effects, Events, and React error handlers (e.g. `hydrateRoot#options.onCaughtError`) Owner Stacks should be available. From 884315785a94f04ba8e2aefadfa4c61b6242c594 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Tue, 14 Jan 2025 10:54:53 +0100 Subject: [PATCH 07/20] Remove "When to use which" Was just rephrasing what we already had and gave wrong impression --- src/content/reference/react/captureOwnerStack.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index 406cd01cd0b..f5db32a6d6c 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -130,12 +130,6 @@ Neither `App` nor the DOM components (e.g. `fieldset`) are considered Owners in Neither `Navigation` nor `legend` are in the stack at all since it's only a sibling to a node containing ``. -### When to use which {/*when-to-use-which*/} - -The Component Stack is useful for contextual information e.g. [`React.Suspense`](/reference/react/Suspense), [React Context](https://react.dev/reference/react/createContext), or [``](/reference/react-dom/components/form). - -The Owner Stack is useful for tracing the flow of props. Owner stacks are equivalent to [call stacks](https://developer.mozilla.org/en-US/docs/Glossary/Call_stack) if all JSX would be converted to direct function calls e.g. `Component({label: "disabled"})` instead of ``. - ## Usage {/*usage*/} ### Expanding error stacks {/*expanding-error-stacks*/} From f46f2018b803c367d758e00dd314fc8619155616 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Tue, 14 Jan 2025 10:59:08 +0100 Subject: [PATCH 08/20] move parent vs owner into DeepDive --- src/content/reference/react/captureOwnerStack.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index f5db32a6d6c..3a40aa61181 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -60,9 +60,11 @@ Outside of development builds, `null` is returned. - Owner Stacks are only available in development. `captureOwnerStack` will always return `null` outside of development. -## Owner Stack vs Component Stack {/*owner-stack-vs-component-stack*/} + -The Owner Stack is different from the Component Stack available error handlers like [`errorInfo.componentStack` in `onUncaughtError`](/reference/react-dom/client/hydrateRoot#show-a-dialog-for-uncaught-errors). +#### Owner Stack vs Component Stack {/*owner-stack-vs-component-stack*/} + +The Owner Stack is different from the Component Stack available in React error handlers like [`errorInfo.componentStack` in `onUncaughtError`](/reference/react-dom/client/hydrateRoot#show-a-dialog-for-uncaught-errors). For example, consider the following code: @@ -130,6 +132,8 @@ Neither `App` nor the DOM components (e.g. `fieldset`) are considered Owners in Neither `Navigation` nor `legend` are in the stack at all since it's only a sibling to a node containing ``. + + ## Usage {/*usage*/} ### Expanding error stacks {/*expanding-error-stacks*/} From 4e9ca6f50f639800171e2e81ab5fbde2b2663ac3 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Tue, 14 Jan 2025 14:28:25 +0100 Subject: [PATCH 09/20] Move code into sandboxes --- .../reference/react/captureOwnerStack.md | 180 ++++++++++++++---- 1 file changed, 147 insertions(+), 33 deletions(-) diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index 3a40aa61181..a158ca4291a 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -34,12 +34,12 @@ const stack = captureOwnerStack(); Call `captureOwnerStack` to get the current Owner Stack. -```js -import * as React from 'react'; +```js {5,5} +import {captureOwnerStack} from 'react'; function Component() { if (process.env.NODE_ENV !== 'production') { - const ownerStack = React.captureOwnerStack(); + const ownerStack = captureOwnerStack(); console.log(ownerStack); } } @@ -68,9 +68,10 @@ The Owner Stack is different from the Component Stack available in React error h For example, consider the following code: -```tsx -import * as React from 'react'; -import * as ReactDOMClient from 'react-dom/client'; + + +```js src/App.js +import {Suspense} from 'react'; function SubComponent({disabled}) { if (disabled) { @@ -78,7 +79,7 @@ function SubComponent({disabled}) { } } -function Component({label}) { +export function Component({label}) { return (
{label} @@ -91,24 +92,70 @@ function Navigation() { return null; } -function App({children}) { +export default function App({children}) { return ( - +
{children}
-
+ ); } +``` -createRoot(document.createElement('div')).render( +```js src/index.js +import {captureOwnerStack} from 'react'; +import {createRoot} from 'react-dom/client'; +import App, {Component} from './App.js'; +import './styles.css'; + +createRoot(document.createElement('div'), { + onUncaughtError: (error, errorInfo) => { + // The stacks are logged instead of showing them in the UI directly to highlight that browsers will apply sourcemaps to the logged stacks. + // Note that sourcemapping is only applied in the real browser console not in the fake one displayed on this page. + console.log(errorInfo.componentStack); + console.log(captureOwnerStack()); + }, +}).render( ); ``` +```json package.json hidden +{ + "dependencies": { + "react": "experimental", + "react-dom": "experimental", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + +```html public/index.html hidden + + + + + + Document + + +

Check the console output.

+ + +``` + + + `SubComponent` would throw an error. The Component Stack of that error would be @@ -124,7 +171,6 @@ at App However, the Owner Stack would only read ``` -at SubComponent at Component ``` @@ -132,39 +178,107 @@ Neither `App` nor the DOM components (e.g. `fieldset`) are considered Owners in Neither `Navigation` nor `legend` are in the stack at all since it's only a sibling to a node containing ``. +`SubComponent` is omitted because it's already part of the callstack. + ## Usage {/*usage*/} ### Expanding error stacks {/*expanding-error-stacks*/} -In addition to the stack trace of the error itself, you can use `captureOwnerStack` to append the Owner Stack. +In addition to the stack trace of the error itself, you can use `captureOwnerStack` to append the Owner Stack. This can help trace the error especially when the error is caused by props. The Owner Stack helps trace the flow of props. -```jsx [[9, 15, "error"], [34, 10, "captureOwnerStack"]] -import { captureOwnerStack } from 'react' -import { hydrateRoot } from 'react-dom/client'; - -const root = hydrateRoot( - document.getElementById('root'), - , - { - onCaughtError: (error, errorInfo) => { - if (process.env.NODE_ENV !== 'production') { - const ownerStack = captureOwnerStack(); - error.stack += ownerStack; - } - console.error( - 'Caught error', - error, - errorInfo.componentStack - ); + +```js src/index.js [[1, 8, "error.stack"], [2, 7, "captureOwnerStack()"]] +import {captureOwnerStack} from 'react'; +import {createRoot} from 'react-dom/client'; + +const root = createRoot(document.getElementById('root'), { + onUncaughtError: (error, errorInfo) => { + if (process.env.NODE_ENV !== 'production') { + const ownerStack = captureOwnerStack(); + error.stack += ownerStack; + } + + console.error('Uncaught', error); + }, +}).render(); +``` + + + +```js +function useCustomHook() { + throw new Error('Boom!'); +} + +function Component() { + useCustomHook(); +} + +export default function App() { + return ; +} +``` + +```js src/index.js +import {captureOwnerStack} from 'react'; +import {createRoot} from 'react-dom/client'; +import App from './App.js'; +import './styles.css'; + +const root = createRoot(document.getElementById('root'), { + onUncaughtError: (error, errorInfo) => { + if (process.env.NODE_ENV !== 'production') { + const ownerStack = captureOwnerStack(); + error.stack = + // The stack is only split because these sandboxes don't implement ignore-listing 3rd party frames via sourcemaps. + // A framework would ignore-list stackframes from React via sourcemaps and then you could just `error.stack += ownerStack`. + // To learn more about ignore-listing see https://developer.chrome.com/docs/devtools/x-google-ignore-list + error.stack.split('\n at react-stack-bottom-frame')[0] + ownerStack; } + + // The stacks are logged instead of showing them in the UI directly to highlight that browsers will apply sourcemaps to the logged stacks. + // Note that sourcemapping is only applied in the real browser console not in the fake one displayed on this page. + console.error('Uncaught', error); + }, +}).render(); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "experimental", + "react-dom": "experimental", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" } -); -root.render(); +} +``` + +```html public/index.html hidden + + + + + + Document + + +

Check the console output.

+
+ + ``` +
+ ## Troubleshooting {/*troubleshooting*/} ### The Owner Stack is `null` {/*the-owner-stack-is-null*/} From c8e688ee6a0f6f77c47671cbeb046e4abbf67853 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Tue, 14 Jan 2025 16:37:18 +0100 Subject: [PATCH 10/20] Include `captureOwnerStack` in error handling examples i.e. everywhere I found mention of `"componentStack"` --- src/components/MDX/CodeBlock/CodeBlock.tsx | 1 + src/components/MDX/MDXComponents.tsx | 15 ++ .../reference/react-dom/client/createRoot.md | 142 ++++++++++----- .../reference/react-dom/client/hydrateRoot.md | 164 +++++++++++------- src/content/reference/react/Component.md | 24 ++- .../reference/react/captureOwnerStack.md | 9 + 6 files changed, 249 insertions(+), 106 deletions(-) diff --git a/src/components/MDX/CodeBlock/CodeBlock.tsx b/src/components/MDX/CodeBlock/CodeBlock.tsx index 1fd9a8a90b1..42165c57d83 100644 --- a/src/components/MDX/CodeBlock/CodeBlock.tsx +++ b/src/components/MDX/CodeBlock/CodeBlock.tsx @@ -336,6 +336,7 @@ function getInlineDecorators( line.step === 3, 'bg-green-40 border-green-40 text-green-60 dark:text-green-30': line.step === 4, + // TODO: Some codeblocks use up to 6 steps. } ), }) diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx index f24fac5988e..c92b96c4bd3 100644 --- a/src/components/MDX/MDXComponents.tsx +++ b/src/components/MDX/MDXComponents.tsx @@ -120,6 +120,20 @@ const CanaryBadge = ({title}: {title: string}) => ( ); +const ExperimentalBadge = ({title}: {title: string}) => ( + + + Experimental only + +); + const NextMajorBadge = ({title}: {title: string}) => ( onUncaughtError option is a function called wi 1. The error that was thrown. 2. An errorInfo object that contains the componentStack of the error. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + You can use the `onUncaughtError` root option to display error dialogs: @@ -394,8 +399,10 @@ You can use the `onUncaughtError` root option to display error dialogs:


   

-

This error occurred at:

+

Component stack:


+  

Owner stack:

+

   

Call stack:


   
@@ -465,12 +472,13 @@ pre.nowrap { ``` ```js src/reportError.js hidden -function reportError({ title, error, componentStack, dismissable }) { +function reportError({ title, error, componentStack, ownerStack, 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 errorOwnerStack = document.getElementById("error-owner-stack"); const errorStack = document.getElementById("error-stack"); const errorClose = document.getElementById("error-close"); const errorCause = document.getElementById("error-cause"); @@ -493,6 +501,9 @@ function reportError({ title, error, componentStack, dismissable }) { // Display component stack errorComponentStack.innerText = componentStack; + // Display owner stack + errorOwnerStack.innerText = ownerStack; + // 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]; @@ -518,20 +529,22 @@ function reportError({ title, error, componentStack, dismissable }) { errorDialog.classList.remove("hidden"); } -export function reportCaughtError({error, cause, componentStack}) { - reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +export function reportCaughtError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Caught Error", error, componentStack, ownerStack, dismissable: true}); } -export function reportUncaughtError({error, cause, componentStack}) { - reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +export function reportUncaughtError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Uncaught Error", error, componentStack, ownerStack, dismissable: false }); } -export function reportRecoverableError({error, cause, componentStack}) { - reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +export function reportRecoverableError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Recoverable Error", error, componentStack, ownerStack, dismissable: true }); } ``` ```js src/index.js active +// captureOwnerStack is only available in react@experimental. +import { captureOwnerStack } from 'react'; import { createRoot } from "react-dom/client"; import App from "./App.js"; import {reportUncaughtError} from "./reportError"; @@ -543,7 +556,8 @@ const root = createRoot(container, { if (error.message !== 'Known error') { reportUncaughtError({ error, - componentStack: errorInfo.componentStack + componentStack: errorInfo.componentStack, + ownerStack: captureOwnerStack() }); } } @@ -554,7 +568,7 @@ root.render(); ```js src/App.js import { useState } from 'react'; -export default function App() { +function Component() { const [throwError, setThrowError] = useState(false); if (throwError) { @@ -562,16 +576,35 @@ export default function App() { } return ( -
+ <> This error shows the error dialog: - + + + ); +} + +export default function App() { + return ( +
+
); } ``` +```json package.json hidden +{ + "dependencies": { + "react": "experimental", + "react-dom": "experimental", + "react-scripts": "^5.0.0", + "react-error-boundary": "4.0.3" + } +} +``` + @@ -579,7 +612,9 @@ export default function App() { 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"]] +```js [[1, 8, "onCaughtError"], [2, 8, "error", 1], [3, 8, "errorInfo"], [4, 12, "componentStack"], [5, 13, "captureOwnerStack()"]] +// captureOwnerStack is only available in react@experimental. +import { captureOwnerStack } from 'react'; import { createRoot } from 'react-dom/client'; const root = createRoot( @@ -589,7 +624,8 @@ const root = createRoot( console.error( 'Caught error', error, - errorInfo.componentStack + errorInfo.componentStack, + ownerStack: captureOwnerStack() ); } } @@ -602,6 +638,8 @@ The onCaughtError option is a function called with 1. The error that was caught by the boundary. 2. An errorInfo object that contains the componentStack of the error. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + You can use the `onCaughtError` root option to display error dialogs or filter known errors from logging: @@ -625,8 +663,10 @@ You can use the `onCaughtError` root option to display error dialogs or filter k


   

-

This error occurred at:

+

Component stack:


+  

Owner stack:

+

   

Call stack:


   
@@ -696,12 +736,13 @@ pre.nowrap { ``` ```js src/reportError.js hidden -function reportError({ title, error, componentStack, dismissable }) { +function reportError({ title, error, componentStack, ownerStack, 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 errorOwnerStack = document.getElementById("error-owner-stack"); const errorStack = document.getElementById("error-stack"); const errorClose = document.getElementById("error-close"); const errorCause = document.getElementById("error-cause"); @@ -724,6 +765,9 @@ function reportError({ title, error, componentStack, dismissable }) { // Display component stack errorComponentStack.innerText = componentStack; + // Display owner stack + errorOwnerStack.innerText = ownerStack; + // 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]; @@ -749,20 +793,22 @@ function reportError({ title, error, componentStack, dismissable }) { errorDialog.classList.remove("hidden"); } -export function reportCaughtError({error, cause, componentStack}) { - reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +export function reportCaughtError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Caught Error", error, componentStack, ownerStack, dismissable: true}); } -export function reportUncaughtError({error, cause, componentStack}) { - reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +export function reportUncaughtError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Uncaught Error", error, componentStack, ownerStack, dismissable: false }); } -export function reportRecoverableError({error, cause, componentStack}) { - reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +export function reportRecoverableError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Recoverable Error", error, componentStack, ownerStack, dismissable: true }); } ``` ```js src/index.js active +// captureOwnerStack is only available in react@experimental. +import {captureOwnerStack} from 'react'; import { createRoot } from "react-dom/client"; import App from "./App.js"; import {reportCaughtError} from "./reportError"; @@ -775,6 +821,7 @@ const root = createRoot(container, { reportCaughtError({ error, componentStack: errorInfo.componentStack, + ownerStack: captureOwnerStack() }); } } @@ -842,12 +889,11 @@ function Throw({error}) { ```json package.json hidden { "dependencies": { - "react": "19.0.0-rc-3edc000d-20240926", - "react-dom": "19.0.0-rc-3edc000d-20240926", + "react": "experimental", + "react-dom": "experimental", "react-scripts": "^5.0.0", "react-error-boundary": "4.0.3" - }, - "main": "/index.js" + } } ``` @@ -857,7 +903,9 @@ function Throw({error}) { 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"]] +```js [[1, 8, "onRecoverableError"], [2, 8, "error", 1], [3, 12, "error.cause"], [4, 8, "errorInfo"], [5, 13, "componentStack"], [6, 14, "captureOwnerStack()"]] +// captureOwnerStack is only available in react@experimental. +import { captureOwnerStack } from 'react'; import { createRoot } from 'react-dom/client'; const root = createRoot( @@ -869,6 +917,7 @@ const root = createRoot( error, error.cause, errorInfo.componentStack, + captureOwnerStack(), ); } } @@ -881,6 +930,8 @@ The onRecoverableError option is a function called 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. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + You can use the `onRecoverableError` root option to display error dialogs: @@ -904,8 +955,10 @@ You can use the `onRecoverableError` root option to display error dialogs:


   

-

This error occurred at:

+

Component stack:


+  

Owner stack:

+

   

Call stack:


   
@@ -975,12 +1028,13 @@ pre.nowrap { ``` ```js src/reportError.js hidden -function reportError({ title, error, componentStack, dismissable }) { +function reportError({ title, error, componentStack, ownerStack, 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 errorOwnerStack = document.getElementById("error-owner-stack"); const errorStack = document.getElementById("error-stack"); const errorClose = document.getElementById("error-close"); const errorCause = document.getElementById("error-cause"); @@ -1003,6 +1057,9 @@ function reportError({ title, error, componentStack, dismissable }) { // Display component stack errorComponentStack.innerText = componentStack; + // Display owner stack + errorOwnerStack.innerText = ownerStack; + // 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]; @@ -1028,20 +1085,22 @@ function reportError({ title, error, componentStack, dismissable }) { errorDialog.classList.remove("hidden"); } -export function reportCaughtError({error, cause, componentStack}) { - reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +export function reportCaughtError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Caught Error", error, componentStack, ownerStack, dismissable: true}); } -export function reportUncaughtError({error, cause, componentStack}) { - reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +export function reportUncaughtError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Uncaught Error", error, componentStack, ownerStack, dismissable: false }); } -export function reportRecoverableError({error, cause, componentStack}) { - reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +export function reportRecoverableError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Recoverable Error", error, componentStack, ownerStack, dismissable: true }); } ``` ```js src/index.js active +// captureOwnerStack is only available in react@experimental. +import {captureOwnerStack} from 'react' import { createRoot } from "react-dom/client"; import App from "./App.js"; import {reportRecoverableError} from "./reportError"; @@ -1054,6 +1113,7 @@ const root = createRoot(container, { error, cause: error.cause, componentStack: errorInfo.componentStack, + ownerStack: captureOwnerStack(), }); } }); @@ -1100,8 +1160,8 @@ function Throw({error}) { ```json package.json hidden { "dependencies": { - "react": "19.0.0-rc-3edc000d-20240926", - "react-dom": "19.0.0-rc-3edc000d-20240926", + "react": "experimental", + "react-dom": "experimental", "react-scripts": "^5.0.0", "react-error-boundary": "4.0.3" }, diff --git a/src/content/reference/react-dom/client/hydrateRoot.md b/src/content/reference/react-dom/client/hydrateRoot.md index b1eeca30ce6..914337e371b 100644 --- a/src/content/reference/react-dom/client/hydrateRoot.md +++ b/src/content/reference/react-dom/client/hydrateRoot.md @@ -378,10 +378,12 @@ It is uncommon to call [`root.render`](#root-render) on a hydrated root. Usually 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"]] +```js [[1, 9, "onUncaughtError"], [2, 9, "error", 1], [3, 9, "errorInfo"], [4, 13, "componentStack"], [5, 14, "captureOwnerStack()"]] +// captureOwnerStack is only available in react@experimental. +import { captureOwnerStack } from 'react'; import { hydrateRoot } from 'react-dom/client'; -const root = hydrateRoot( +hydrateRoot( document.getElementById('root'), , { @@ -389,12 +391,12 @@ const root = hydrateRoot( console.error( 'Uncaught error', error, - errorInfo.componentStack + errorInfo.componentStack, + captureOwnerStack() ); } } ); -root.render(); ``` The onUncaughtError option is a function called with two arguments: @@ -402,6 +404,8 @@ The onUncaughtError option is a function called wi 1. The error that was thrown. 2. An errorInfo object that contains the componentStack of the error. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + You can use the `onUncaughtError` root option to display error dialogs: @@ -425,8 +429,10 @@ You can use the `onUncaughtError` root option to display error dialogs:


   

-

This error occurred at:

+

Component stack:


+  

Owner stack:

+

   

Call stack:


   
@@ -499,12 +505,13 @@ pre.nowrap { ``` ```js src/reportError.js hidden -function reportError({ title, error, componentStack, dismissable }) { +function reportError({ title, error, componentStack, ownerStack, 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 errorOwnerStack = document.getElementById("error-owner-stack"); const errorStack = document.getElementById("error-stack"); const errorClose = document.getElementById("error-close"); const errorCause = document.getElementById("error-cause"); @@ -527,6 +534,9 @@ function reportError({ title, error, componentStack, dismissable }) { // Display component stack errorComponentStack.innerText = componentStack; + // Display owner stack + errorOwnerStack.innerText = ownerStack; + // 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]; @@ -552,20 +562,22 @@ function reportError({ title, error, componentStack, dismissable }) { errorDialog.classList.remove("hidden"); } -export function reportCaughtError({error, cause, componentStack}) { - reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +export function reportCaughtError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Caught Error", error, componentStack, ownerStack, dismissable: true}); } -export function reportUncaughtError({error, cause, componentStack}) { - reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +export function reportUncaughtError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Uncaught Error", error, componentStack, ownerStack, dismissable: false }); } -export function reportRecoverableError({error, cause, componentStack}) { - reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +export function reportRecoverableError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Recoverable Error", error, componentStack, ownerStack, ownerStack, dismissable: true }); } ``` ```js src/index.js active +// captureOwnerStack is only available in react@experimental. +import { captureOwnerStack } from 'react'; import { hydrateRoot } from "react-dom/client"; import App from "./App.js"; import {reportUncaughtError} from "./reportError"; @@ -573,12 +585,13 @@ import "./styles.css"; import {renderToString} from 'react-dom/server'; const container = document.getElementById("root"); -const root = hydrateRoot(container, , { +hydrateRoot(container, , { onUncaughtError: (error, errorInfo) => { if (error.message !== 'Known error') { reportUncaughtError({ error, - componentStack: errorInfo.componentStack + componentStack: errorInfo.componentStack, + ownerStack: captureOwnerStack() }); } } @@ -588,7 +601,7 @@ const root = hydrateRoot(container, , { ```js src/App.js import { useState } from 'react'; -export default function App() { +function Component() { const [throwError, setThrowError] = useState(false); if (throwError) { @@ -596,16 +609,35 @@ export default function App() { } return ( -
+ <> This error shows the error dialog: - + + + ); +} + +export default function App() { + return ( +
+
); } ``` +```json package.json hidden +{ + "dependencies": { + "react": "experimental", + "react-dom": "experimental", + "react-scripts": "^5.0.0", + "react-error-boundary": "4.0.3" + } +} +``` + @@ -613,10 +645,12 @@ export default function App() { 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"]] +```js [[1, 9, "onCaughtError"], [2, 9, "error", 1], [3, 9, "errorInfo"], [4, 13, "componentStack"], [5, 14, "captureOwnerStack()"]] +// captureOwnerStack is only available in react@experimental. +import { captureOwnerStack } from 'react'; import { hydrateRoot } from 'react-dom/client'; -const root = hydrateRoot( +hydrateRoot( document.getElementById('root'), , { @@ -624,12 +658,12 @@ const root = hydrateRoot( console.error( 'Caught error', error, - errorInfo.componentStack + errorInfo.componentStack, + ownerStack: captureOwnerStack() ); } } ); -root.render(); ``` The onCaughtError option is a function called with two arguments: @@ -637,6 +671,8 @@ The onCaughtError option is a function called with 1. The error that was caught by the boundary. 2. An errorInfo object that contains the componentStack of the error. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + You can use the `onCaughtError` root option to display error dialogs or filter known errors from logging: @@ -660,8 +696,10 @@ You can use the `onCaughtError` root option to display error dialogs or filter k


   

-

This error occurred at:

+

Component stack:


+  

Owner stack:

+

   

Call stack:


   
@@ -734,12 +772,13 @@ pre.nowrap { ``` ```js src/reportError.js hidden -function reportError({ title, error, componentStack, dismissable }) { +function reportError({ title, error, componentStack, ownerStack, 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 errorOwnerStack = document.getElementById("error-owner-stack"); const errorStack = document.getElementById("error-stack"); const errorClose = document.getElementById("error-close"); const errorCause = document.getElementById("error-cause"); @@ -762,6 +801,9 @@ function reportError({ title, error, componentStack, dismissable }) { // Display component stack errorComponentStack.innerText = componentStack; + // Display owner stack + errorOwnerStack.innerText = ownerStack; + // 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]; @@ -787,32 +829,35 @@ function reportError({ title, error, componentStack, dismissable }) { errorDialog.classList.remove("hidden"); } -export function reportCaughtError({error, cause, componentStack}) { - reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +export function reportCaughtError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Caught Error", error, componentStack, ownerStack, dismissable: true}); } export function reportUncaughtError({error, cause, componentStack}) { - reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); + reportError({ title: "Uncaught Error", error, componentStack, ownerStack, dismissable: false }); } export function reportRecoverableError({error, cause, componentStack}) { - reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); + reportError({ title: "Recoverable Error", error, componentStack, ownerStack, ownerStack, dismissable: true }); } ``` ```js src/index.js active +// captureOwnerStack is only available in react@experimental. +import {captureOwnerStack} from 'react'; 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, , { +hydrateRoot(container, , { onCaughtError: (error, errorInfo) => { if (error.message !== 'Known error') { reportCaughtError({ error, - componentStack: errorInfo.componentStack + componentStack: errorInfo.componentStack, + ownerStack: captureOwnerStack() }); } } @@ -879,12 +924,11 @@ function Throw({error}) { ```json package.json hidden { "dependencies": { - "react": "19.0.0-rc-3edc000d-20240926", - "react-dom": "19.0.0-rc-3edc000d-20240926", + "react": "experimental", + "react-dom": "experimental", "react-scripts": "^5.0.0", "react-error-boundary": "4.0.3" - }, - "main": "/index.js" + } } ``` @@ -894,7 +938,9 @@ function Throw({error}) { 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"]] +```js [[1, 9, "onRecoverableError"], [2, 9, "error", 1], [3, 13, "error.cause", 1], [4, 9, "errorInfo"], [5, 14, "componentStack"], [6, 15, "captureOwnerStack()"]] +// captureOwnerStack is only available in react@experimental. +import { captureOwnerStack } from 'react'; import { hydrateRoot } from 'react-dom/client'; const root = hydrateRoot( @@ -906,7 +952,8 @@ const root = hydrateRoot( 'Caught error', error, error.cause, - errorInfo.componentStack + errorInfo.componentStack, + captureOwnerStack() ); } } @@ -918,6 +965,8 @@ The onRecoverableError option is a function called 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. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + You can use the `onRecoverableError` root option to display error dialogs for hydration mismatches: @@ -941,8 +990,10 @@ You can use the `onRecoverableError` root option to display error dialogs for hy


   

-

This error occurred at:

+

Component stack:


+  

Owner stack:

+

   

Call stack:


   
@@ -1015,12 +1066,13 @@ pre.nowrap { ``` ```js src/reportError.js hidden -function reportError({ title, error, componentStack, dismissable }) { +function reportError({ title, error, componentStack, ownerStack, 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 errorOwnerStack = document.getElementById("error-owner-stack"); const errorStack = document.getElementById("error-stack"); const errorClose = document.getElementById("error-close"); const errorCause = document.getElementById("error-cause"); @@ -1043,6 +1095,9 @@ function reportError({ title, error, componentStack, dismissable }) { // Display component stack errorComponentStack.innerText = componentStack; + // Display owner stack + errorOwnerStack.innerText = ownerStack; + // 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]; @@ -1068,20 +1123,22 @@ function reportError({ title, error, componentStack, dismissable }) { errorDialog.classList.remove("hidden"); } -export function reportCaughtError({error, cause, componentStack}) { - reportError({ title: "Caught Error", error, componentStack, dismissable: true}); +export function reportCaughtError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Caught Error", error, componentStack, ownerStack, dismissable: true}); } -export function reportUncaughtError({error, cause, componentStack}) { - reportError({ title: "Uncaught Error", error, componentStack, dismissable: false }); +export function reportUncaughtError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Uncaught Error", error, componentStack, ownerStack, dismissable: false }); } -export function reportRecoverableError({error, cause, componentStack}) { - reportError({ title: "Recoverable Error", error, componentStack, dismissable: true }); +export function reportRecoverableError({error, cause, componentStack, ownerStack}) { + reportError({ title: "Recoverable Error", error, componentStack, ownerStack, dismissable: true }); } ``` ```js src/index.js active +// captureOwnerStack is only available in react@experimental. +import {captureOwnerStack} from 'react' import { hydrateRoot } from "react-dom/client"; import App from "./App.js"; import {reportRecoverableError} from "./reportError"; @@ -1093,7 +1150,8 @@ const root = hydrateRoot(container, , { reportRecoverableError({ error, cause: error.cause, - componentStack: errorInfo.componentStack + componentStack: errorInfo.componentStack, + ownerStack: captureOwnerStack() }); } }); @@ -1104,16 +1162,6 @@ 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'} ); @@ -1141,8 +1189,8 @@ function Throw({error}) { ```json package.json hidden { "dependencies": { - "react": "19.0.0-rc-3edc000d-20240926", - "react-dom": "19.0.0-rc-3edc000d-20240926", + "react": "experimental", + "react-dom": "experimental", "react-scripts": "^5.0.0", "react-error-boundary": "4.0.3" }, diff --git a/src/content/reference/react/Component.md b/src/content/reference/react/Component.md index 99fa17986f0..5e8c8d9679f 100644 --- a/src/content/reference/react/Component.md +++ b/src/content/reference/react/Component.md @@ -1273,7 +1273,11 @@ By default, if your application throws an error during rendering, React will rem To implement an error boundary component, you need to provide [`static getDerivedStateFromError`](#static-getderivedstatefromerror) which lets you update state in response to an error and display an error message to the user. You can also optionally implement [`componentDidCatch`](#componentdidcatch) to add some extra logic, for example, to log the error to an analytics service. -```js {7-10,12-19} + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + +```js {9-12,14-27} +import * as React from 'react'; + class ErrorBoundary extends React.Component { constructor(props) { super(props); @@ -1286,12 +1290,18 @@ class ErrorBoundary extends React.Component { } componentDidCatch(error, info) { - // Example "componentStack": - // in ComponentThatThrows (created by App) - // in ErrorBoundary (created by App) - // in div (created by App) - // in App - logErrorToMyService(error, info.componentStack); + logErrorToMyService( + error, + // Example "componentStack": + // in ComponentThatThrows (created by App) + // in ErrorBoundary (created by App) + // in div (created by App) + // in App + info.componentStack, + // Only available in react@experimental. + // Warning: Owner Stack is not available in production. + React.captureOwnerStack(), + ); } render() { diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index a158ca4291a..87665303586 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -184,6 +184,15 @@ Neither `Navigation` nor `legend` are in the stack at all since it's only a sibl ## Usage {/*usage*/} +### Improve error reporting {/*improve-error-reporting*/} + +Check out the following example to see how to use `captureOwnerStack` to improve error reporting: + +- [createRoot usage: Show a dialog for uncaught errors +](/reference/react-dom/client/createRoot#show-a-dialog-for-uncaught-errors) +- [createRoot usage: Displaying Error Boundary errors](/reference/react-dom/client/createRoot#show-a-dialog-for-uncaught-errors) +- [createRoot usage: Displaying a dialog for recoverable errors](/reference/react-dom/client/createRoot#displaying-a-dialog-for-recoverable-errors) + ### Expanding error stacks {/*expanding-error-stacks*/} In addition to the stack trace of the error itself, you can use `captureOwnerStack` to append the Owner Stack. From 5b8bc8771822011e6c48698feb50980962b86f97 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 23 Jan 2025 14:11:59 +0100 Subject: [PATCH 11/20] experimental -> Canary --- src/components/MDX/MDXComponents.tsx | 15 --------------- .../reference/react-dom/client/createRoot.md | 18 +++++++++--------- .../reference/react-dom/client/hydrateRoot.md | 18 +++++++++--------- src/content/reference/react/Component.md | 4 ++-- .../reference/react/captureOwnerStack.md | 12 +++--------- 5 files changed, 23 insertions(+), 44 deletions(-) diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx index c92b96c4bd3..f24fac5988e 100644 --- a/src/components/MDX/MDXComponents.tsx +++ b/src/components/MDX/MDXComponents.tsx @@ -120,20 +120,6 @@ const CanaryBadge = ({title}: {title: string}) => ( ); -const ExperimentalBadge = ({title}: {title: string}) => ( - - - Experimental only - -); - const NextMajorBadge = ({title}: {title: string}) => ( onUncaughtError option is a function called wi 1. The error that was thrown. 2. An errorInfo object that contains the componentStack of the error. - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. You can use the `onUncaughtError` root option to display error dialogs: @@ -543,7 +543,7 @@ export function reportRecoverableError({error, cause, componentStack, ownerStack ``` ```js src/index.js active -// captureOwnerStack is only available in react@experimental. +// captureOwnerStack is only available in react@canary. import { captureOwnerStack } from 'react'; import { createRoot } from "react-dom/client"; import App from "./App.js"; @@ -613,7 +613,7 @@ export default function App() { 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, 8, "onCaughtError"], [2, 8, "error", 1], [3, 8, "errorInfo"], [4, 12, "componentStack"], [5, 13, "captureOwnerStack()"]] -// captureOwnerStack is only available in react@experimental. +// captureOwnerStack is only available in react@canary. import { captureOwnerStack } from 'react'; import { createRoot } from 'react-dom/client'; @@ -638,7 +638,7 @@ The onCaughtError option is a function called with 1. The error that was caught by the boundary. 2. An errorInfo object that contains the componentStack of the error. - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. You can use the `onCaughtError` root option to display error dialogs or filter known errors from logging: @@ -807,7 +807,7 @@ export function reportRecoverableError({error, cause, componentStack, ownerStack ``` ```js src/index.js active -// captureOwnerStack is only available in react@experimental. +// captureOwnerStack is only available in react@canary. import {captureOwnerStack} from 'react'; import { createRoot } from "react-dom/client"; import App from "./App.js"; @@ -904,7 +904,7 @@ function Throw({error}) { 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, 8, "onRecoverableError"], [2, 8, "error", 1], [3, 12, "error.cause"], [4, 8, "errorInfo"], [5, 13, "componentStack"], [6, 14, "captureOwnerStack()"]] -// captureOwnerStack is only available in react@experimental. +// captureOwnerStack is only available in react@canary. import { captureOwnerStack } from 'react'; import { createRoot } from 'react-dom/client'; @@ -930,7 +930,7 @@ The onRecoverableError option is a function called 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. - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. You can use the `onRecoverableError` root option to display error dialogs: @@ -1099,7 +1099,7 @@ export function reportRecoverableError({error, cause, componentStack, ownerStack ``` ```js src/index.js active -// captureOwnerStack is only available in react@experimental. +// captureOwnerStack is only available in react@canary. import {captureOwnerStack} from 'react' import { createRoot } from "react-dom/client"; import App from "./App.js"; diff --git a/src/content/reference/react-dom/client/hydrateRoot.md b/src/content/reference/react-dom/client/hydrateRoot.md index 914337e371b..1f47b18514e 100644 --- a/src/content/reference/react-dom/client/hydrateRoot.md +++ b/src/content/reference/react-dom/client/hydrateRoot.md @@ -379,7 +379,7 @@ It is uncommon to call [`root.render`](#root-render) on a hydrated root. Usually 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, 9, "onUncaughtError"], [2, 9, "error", 1], [3, 9, "errorInfo"], [4, 13, "componentStack"], [5, 14, "captureOwnerStack()"]] -// captureOwnerStack is only available in react@experimental. +// captureOwnerStack is only available in react@canary. import { captureOwnerStack } from 'react'; import { hydrateRoot } from 'react-dom/client'; @@ -404,7 +404,7 @@ The onUncaughtError option is a function called wi 1. The error that was thrown. 2. An errorInfo object that contains the componentStack of the error. - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. You can use the `onUncaughtError` root option to display error dialogs: @@ -576,7 +576,7 @@ export function reportRecoverableError({error, cause, componentStack, ownerStack ``` ```js src/index.js active -// captureOwnerStack is only available in react@experimental. +// captureOwnerStack is only available in react@canary. import { captureOwnerStack } from 'react'; import { hydrateRoot } from "react-dom/client"; import App from "./App.js"; @@ -646,7 +646,7 @@ export default function App() { 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, 9, "onCaughtError"], [2, 9, "error", 1], [3, 9, "errorInfo"], [4, 13, "componentStack"], [5, 14, "captureOwnerStack()"]] -// captureOwnerStack is only available in react@experimental. +// captureOwnerStack is only available in react@canary. import { captureOwnerStack } from 'react'; import { hydrateRoot } from 'react-dom/client'; @@ -671,7 +671,7 @@ The onCaughtError option is a function called with 1. The error that was caught by the boundary. 2. An errorInfo object that contains the componentStack of the error. - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. You can use the `onCaughtError` root option to display error dialogs or filter known errors from logging: @@ -843,7 +843,7 @@ export function reportRecoverableError({error, cause, componentStack}) { ``` ```js src/index.js active -// captureOwnerStack is only available in react@experimental. +// captureOwnerStack is only available in react@canary. import {captureOwnerStack} from 'react'; import { hydrateRoot } from "react-dom/client"; import App from "./App.js"; @@ -939,7 +939,7 @@ function Throw({error}) { 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, 9, "onRecoverableError"], [2, 9, "error", 1], [3, 13, "error.cause", 1], [4, 9, "errorInfo"], [5, 14, "componentStack"], [6, 15, "captureOwnerStack()"]] -// captureOwnerStack is only available in react@experimental. +// captureOwnerStack is only available in react@canary. import { captureOwnerStack } from 'react'; import { hydrateRoot } from 'react-dom/client'; @@ -965,7 +965,7 @@ The onRecoverableError option is a function called 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. - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. You can use the `onRecoverableError` root option to display error dialogs for hydration mismatches: @@ -1137,7 +1137,7 @@ export function reportRecoverableError({error, cause, componentStack, ownerStack ``` ```js src/index.js active -// captureOwnerStack is only available in react@experimental. +// captureOwnerStack is only available in react@canary. import {captureOwnerStack} from 'react' import { hydrateRoot } from "react-dom/client"; import App from "./App.js"; diff --git a/src/content/reference/react/Component.md b/src/content/reference/react/Component.md index 5e8c8d9679f..0821d159392 100644 --- a/src/content/reference/react/Component.md +++ b/src/content/reference/react/Component.md @@ -1273,7 +1273,7 @@ By default, if your application throws an error during rendering, React will rem To implement an error boundary component, you need to provide [`static getDerivedStateFromError`](#static-getderivedstatefromerror) which lets you update state in response to an error and display an error message to the user. You can also optionally implement [`componentDidCatch`](#componentdidcatch) to add some extra logic, for example, to log the error to an analytics service. - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. + With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. ```js {9-12,14-27} import * as React from 'react'; @@ -1298,7 +1298,7 @@ class ErrorBoundary extends React.Component { // in div (created by App) // in App info.componentStack, - // Only available in react@experimental. + // Only available in react@canary. // Warning: Owner Stack is not available in production. React.captureOwnerStack(), ); diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index 87665303586..57ceb54b322 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -2,17 +2,11 @@ title: captureOwnerStack --- - + -**This API is experimental and is not available in a stable version of React yet.** +The `captureOwnerStack` API is currently only available in React's Canary and experimental channels. Learn more about [React's release channels here](/community/versioning-policy#all-release-channels). -You can try it by upgrading React packages to the most recent experimental version: - -- `react@experimental` - -Experimental versions of React may contain bugs. Don't use them in production. - - + From 2d0da579b9b0ce88bae31dfed400e1c592404681 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 23 Jan 2025 14:19:57 +0100 Subject: [PATCH 12/20] repeat when owner stacks are available --- src/content/reference/react/captureOwnerStack.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index 57ceb54b322..027c3979dba 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -47,8 +47,7 @@ function Component() { `captureOwnerStack` returns `string | null`. -If no Owner Stack is available, it returns an empty string. -Outside of development builds, `null` is returned. +If no Owner Stack is available (outside of render, Effects, Events and React error handlers), it returns an empty string (see [Troubleshooting: The Owner Stack is empty](#the-owner-stack-is-empty-the-owner-stack-is-empty)). Outside of development builds, `null` is returned (see [Troubleshooting: The Owner Stack is `null`](#the-owner-stack-is-null-the-owner-stack-is-null)). #### Caveats {/*caveats*/} From 9806ea7711317473c813eedf8d951338349445d0 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 23 Jan 2025 14:22:20 +0100 Subject: [PATCH 13/20] Work around single newlines being rendered as newlines Only double newlines usually end up as a newline in rendered markdown. --- src/content/reference/react/captureOwnerStack.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index 027c3979dba..73db2cf9d71 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -285,10 +285,8 @@ const root = createRoot(document.getElementById('root'), { ### The Owner Stack is `null` {/*the-owner-stack-is-null*/} -`captureOwnerStack` was called outside of development builds. -For performance reasons, React will not keep track of Owners in production. +`captureOwnerStack` was called outside of development builds. For performance reasons, React will not keep track of Owners in production. ### The Owner Stack is empty {/*the-owner-stack-is-empty*/} -The call of `captureOwnerStack` happened outside of a React controlled function e.g. in a `setTimeout` callback. -During render, Effects, Events, and React error handlers (e.g. `hydrateRoot#options.onCaughtError`) Owner Stacks should be available. +The call of `captureOwnerStack` happened outside of a React controlled function e.g. in a `setTimeout` callback. During render, Effects, Events, and React error handlers (e.g. `hydrateRoot#options.onCaughtError`) Owner Stacks should be available. From 66562e88cccd709fa5af6c111f876195e437f43b Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 23 Jan 2025 14:24:30 +0100 Subject: [PATCH 14/20] Ensure comments don't hscroll --- .../reference/react/captureOwnerStack.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index 73db2cf9d71..df5c3e85c85 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -105,8 +105,10 @@ import './styles.css'; createRoot(document.createElement('div'), { onUncaughtError: (error, errorInfo) => { - // The stacks are logged instead of showing them in the UI directly to highlight that browsers will apply sourcemaps to the logged stacks. - // Note that sourcemapping is only applied in the real browser console not in the fake one displayed on this page. + // The stacks are logged instead of showing them in the UI directly to + // highlight that browsers will apply sourcemaps to the logged stacks. + // Note that sourcemapping is only applied in the real browser console not + // in the fake one displayed on this page. console.log(errorInfo.componentStack); console.log(captureOwnerStack()); }, @@ -235,14 +237,18 @@ const root = createRoot(document.getElementById('root'), { if (process.env.NODE_ENV !== 'production') { const ownerStack = captureOwnerStack(); error.stack = - // The stack is only split because these sandboxes don't implement ignore-listing 3rd party frames via sourcemaps. - // A framework would ignore-list stackframes from React via sourcemaps and then you could just `error.stack += ownerStack`. + // The stack is only split because these sandboxes don't implement + // ignore-listing 3rd party frames via sourcemaps. + // A framework would ignore-list stackframes from React via sourcemaps + // and then you could just `error.stack += ownerStack`. // To learn more about ignore-listing see https://developer.chrome.com/docs/devtools/x-google-ignore-list error.stack.split('\n at react-stack-bottom-frame')[0] + ownerStack; } - // The stacks are logged instead of showing them in the UI directly to highlight that browsers will apply sourcemaps to the logged stacks. - // Note that sourcemapping is only applied in the real browser console not in the fake one displayed on this page. + // The stacks are logged instead of showing them in the UI directly to + // highlight that browsers will apply sourcemaps to the logged stacks. + // Note that sourcemapping is only applied in the real browser console not + // in the fake one displayed on this page. console.error('Uncaught', error); }, }).render(); From a2fab1db9f4fc3d1da0af98cac705428a9839bbe Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 23 Jan 2025 14:29:27 +0100 Subject: [PATCH 15/20] Troubleshooting empty stacks sandbox --- .../reference/react/captureOwnerStack.md | 258 +++++++++++++++++- 1 file changed, 250 insertions(+), 8 deletions(-) diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index df5c3e85c85..e967019ce58 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -47,7 +47,13 @@ function Component() { `captureOwnerStack` returns `string | null`. -If no Owner Stack is available (outside of render, Effects, Events and React error handlers), it returns an empty string (see [Troubleshooting: The Owner Stack is empty](#the-owner-stack-is-empty-the-owner-stack-is-empty)). Outside of development builds, `null` is returned (see [Troubleshooting: The Owner Stack is `null`](#the-owner-stack-is-null-the-owner-stack-is-null)). +Owner Stacks are available in +- Component render +- Effects (e.g. `useEffect`) +- React's event handlers (e.g. ` +
+ +
+ + + +``` + +```js src/errorOverlay.js + +export function onConsoleError({ consoleMessage, ownerStack }) { + const errorDialog = document.getElementById("error-dialog"); + const errorBody = document.getElementById("error-body"); + const errorOwnerStack = document.getElementById("error-owner-stack"); + const errorStack = document.getElementById("error-stack"); + + // Display console.error() message + errorBody.innerText = consoleMessage; + + // Display owner stack + errorOwnerStack.innerText = ownerStack; + + // Show the dialog + errorDialog.classList.remove("hidden"); +} +``` + +```js src/index.js active +import { captureOwnerStack } from "react"; +import { createRoot } from "react-dom/client"; +import App from './App'; +import { onConsoleError } from "./errorOverlay"; +import './styles.css'; + +const originalConsoleError = console.error; +console.error = function patchedConsoleError(...args) { + originalConsoleError.apply(console, args); + const ownerStack = captureOwnerStack(); + onConsoleError({ + // Keep in mind that in a real application, console.error can be + // called with multiple arguments which you should account for. + consoleMessage: args[0], + ownerStack, + }); +}; + +const container = document.getElementById("root"); +createRoot(container).render(); +``` + +```json package.json hidden +{ + "dependencies": { + "react": "experimental", + "react-dom": "experimental", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + +```js src/App.js +function Component() { + return ; +} -Check out the following example to see how to use `captureOwnerStack` to improve error reporting: +export default function App() { + return ; +} +``` -- [createRoot usage: Show a dialog for uncaught errors -](/reference/react-dom/client/createRoot#show-a-dialog-for-uncaught-errors) -- [createRoot usage: Displaying Error Boundary errors](/reference/react-dom/client/createRoot#show-a-dialog-for-uncaught-errors) -- [createRoot usage: Displaying a dialog for recoverable errors](/reference/react-dom/client/createRoot#displaying-a-dialog-for-recoverable-errors) +
### Expanding error stacks {/*expanding-error-stacks*/} @@ -295,4 +493,48 @@ const root = createRoot(document.getElementById('root'), { ### The Owner Stack is empty {/*the-owner-stack-is-empty*/} -The call of `captureOwnerStack` happened outside of a React controlled function e.g. in a `setTimeout` callback. During render, Effects, Events, and React error handlers (e.g. `hydrateRoot#options.onCaughtError`) Owner Stacks should be available. +The call of `captureOwnerStack` happened outside of a React controlled function e.g. in a `setTimeout` callback, after a fetch or in a custom DOM event handler. During render, Effects, React event handlers, and React error handlers (e.g. `hydrateRoot#options.onCaughtError`) Owner Stacks should be available. + +In the example below, clicking the button will log an empty Owner Stack because `captureOwnerStack` was called during a custom DOM event handler. The Owner Stack must be captured earlier e.g. by moving the call of `captureOwnerStack` into the Effect body. + + +```js +import {captureOwnerStack, useEffect} from 'react'; + +export default function App() { + useEffect(() => { + // Should call `captureOwnerStack` here. + function handleEvent() { + // Calling it in a custom DOM event handler is too late. + // The Owner Stack will be empty at this point. + console.log('Owner Stack: ', captureOwnerStack()); + } + + document.addEventListener('click', handleEvent); + + return () => { + document.removeEventListener('click', handleEvent); + } + }) + + return ; +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "experimental", + "react-dom": "experimental", + "react-scripts": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + + \ No newline at end of file From f4c02461f3966fcdeba02d6fa776018e0893f43a Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 23 Jan 2025 15:21:46 +0100 Subject: [PATCH 16/20] Align codestep previews with sandboxes --- .../reference/react-dom/client/createRoot.md | 87 +++++++++---------- .../reference/react-dom/client/hydrateRoot.md | 86 +++++++++--------- 2 files changed, 83 insertions(+), 90 deletions(-) diff --git a/src/content/reference/react-dom/client/createRoot.md b/src/content/reference/react-dom/client/createRoot.md index 03161d98e85..1bcee74298a 100644 --- a/src/content/reference/react-dom/client/createRoot.md +++ b/src/content/reference/react-dom/client/createRoot.md @@ -348,25 +348,24 @@ It is uncommon to call `render` multiple times. Usually, your components will [u 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, 8, "onUncaughtError"], [2, 8, "error", 1], [3, 8, "errorInfo"], [4, 12, "componentStack"], [5, 13, "captureOwnerStack()"]] +```js [[1, 8, "onUncaughtError"], [2, 8, "error", 1], [3, 8, "errorInfo"], [4, 12, "componentStack", 15], [5, 13, "captureOwnerStack()"]] // captureOwnerStack is only available in react@canary. -import { captureOwnerStack } from 'react'; -import { createRoot } from 'react-dom/client'; +import { captureOwnerStack } from "react"; +import { createRoot } from "react-dom/client"; +import { reportUncaughtError } from "./reportError"; -const root = createRoot( - document.getElementById('root'), - { - onUncaughtError: (error, errorInfo) => { - console.error( - 'Uncaught error', +const container = document.getElementById("root"); +const root = createRoot(container, { + onUncaughtError: (error, errorInfo) => { + if (error.message !== "Known error") { + reportUncaughtError({ error, - errorInfo.componentStack, - captureOwnerStack() - ); + componentStack: errorInfo.componentStack, + ownerStack: captureOwnerStack(), + }); } - } -); -root.render(); + }, +}); ``` The onUncaughtError option is a function called with two arguments: @@ -612,25 +611,24 @@ export default function App() { 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, 8, "onCaughtError"], [2, 8, "error", 1], [3, 8, "errorInfo"], [4, 12, "componentStack"], [5, 13, "captureOwnerStack()"]] +```js [[1, 8, "onCaughtError"], [2, 8, "error", 1], [3, 8, "errorInfo"], [4, 12, "componentStack", 15], [5, 13, "captureOwnerStack()"]] // captureOwnerStack is only available in react@canary. -import { captureOwnerStack } from 'react'; -import { createRoot } from 'react-dom/client'; +import {captureOwnerStack} from 'react'; +import { createRoot } from "react-dom/client"; +import {reportCaughtError} from "./reportError"; -const root = createRoot( - document.getElementById('root'), - { - onCaughtError: (error, errorInfo) => { - console.error( - 'Caught error', - error, - errorInfo.componentStack, +const container = document.getElementById("root"); +const root = createRoot(container, { + onCaughtError: (error, errorInfo) => { + if (error.message !== 'Known error') { + reportCaughtError({ + error, + componentStack: errorInfo.componentStack, ownerStack: captureOwnerStack() - ); + }); } } -); -root.render(); +}); ``` The onCaughtError option is a function called with two arguments: @@ -903,26 +901,23 @@ function Throw({error}) { 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, 8, "onRecoverableError"], [2, 8, "error", 1], [3, 12, "error.cause"], [4, 8, "errorInfo"], [5, 13, "componentStack"], [6, 14, "captureOwnerStack()"]] +```js [[1, 8, "onRecoverableError"], [2, 8, "error", 1], [3, 11, "error.cause"], [4, 8, "errorInfo"], [5, 12, "componentStack", 15], [6, 13, "captureOwnerStack()"]] // captureOwnerStack is only available in react@canary. -import { captureOwnerStack } from 'react'; -import { createRoot } from 'react-dom/client'; +import {captureOwnerStack} from 'react' +import { createRoot } from "react-dom/client"; +import {reportRecoverableError} from "./reportError"; -const root = createRoot( - document.getElementById('root'), - { - onRecoverableError: (error, errorInfo) => { - console.error( - 'Recoverable error', - error, - error.cause, - errorInfo.componentStack, - captureOwnerStack(), - ); - } +const container = document.getElementById("root"); +const root = createRoot(container, { + onRecoverableError: (error, errorInfo) => { + reportRecoverableError({ + error, + cause: error.cause, + componentStack: errorInfo.componentStack, + ownerStack: captureOwnerStack(), + }); } -); -root.render(); +}); ``` The onRecoverableError option is a function called with two arguments: diff --git a/src/content/reference/react-dom/client/hydrateRoot.md b/src/content/reference/react-dom/client/hydrateRoot.md index 1f47b18514e..83d95a82cd8 100644 --- a/src/content/reference/react-dom/client/hydrateRoot.md +++ b/src/content/reference/react-dom/client/hydrateRoot.md @@ -378,25 +378,25 @@ It is uncommon to call [`root.render`](#root-render) on a hydrated root. Usually 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, 9, "onUncaughtError"], [2, 9, "error", 1], [3, 9, "errorInfo"], [4, 13, "componentStack"], [5, 14, "captureOwnerStack()"]] +```js [[1, 9, "onUncaughtError"], [2, 9, "error", 1], [3, 9, "errorInfo"], [4, 13, "componentStack", 15], [5, 14, "captureOwnerStack()"]] // captureOwnerStack is only available in react@canary. import { captureOwnerStack } from 'react'; -import { hydrateRoot } from 'react-dom/client'; +import { hydrateRoot } from "react-dom/client"; +import App from "./App"; +import {reportUncaughtError} from "./reportError"; -hydrateRoot( - document.getElementById('root'), - , - { - onUncaughtError: (error, errorInfo) => { - console.error( - 'Uncaught error', +const container = document.getElementById("root"); +hydrateRoot(container, , { + onUncaughtError: (error, errorInfo) => { + if (error.message !== 'Known error') { + reportUncaughtError({ error, - errorInfo.componentStack, - captureOwnerStack() - ); + componentStack: errorInfo.componentStack, + ownerStack: captureOwnerStack() + }); } } -); +}); ``` The onUncaughtError option is a function called with two arguments: @@ -579,7 +579,7 @@ export function reportRecoverableError({error, cause, componentStack, ownerStack // captureOwnerStack is only available in react@canary. import { captureOwnerStack } from 'react'; import { hydrateRoot } from "react-dom/client"; -import App from "./App.js"; +import App from "./App"; import {reportUncaughtError} from "./reportError"; import "./styles.css"; import {renderToString} from 'react-dom/server'; @@ -645,25 +645,25 @@ export default function App() { 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, 9, "onCaughtError"], [2, 9, "error", 1], [3, 9, "errorInfo"], [4, 13, "componentStack"], [5, 14, "captureOwnerStack()"]] +```js [[1, 9, "onCaughtError"], [2, 9, "error", 1], [3, 9, "errorInfo"], [4, 13, "componentStack", 15], [5, 14, "captureOwnerStack()"]] // captureOwnerStack is only available in react@canary. -import { captureOwnerStack } from 'react'; -import { hydrateRoot } from 'react-dom/client'; +import {captureOwnerStack} from 'react'; +import { hydrateRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportCaughtError} from "./reportError"; -hydrateRoot( - document.getElementById('root'), - , - { - onCaughtError: (error, errorInfo) => { - console.error( - 'Caught error', +const container = document.getElementById("root"); +hydrateRoot(container, , { + onCaughtError: (error, errorInfo) => { + if (error.message !== 'Known error') { + reportCaughtError({ error, - errorInfo.componentStack, + componentStack: errorInfo.componentStack, ownerStack: captureOwnerStack() - ); + }); } } -); +}); ``` The onCaughtError option is a function called with two arguments: @@ -938,26 +938,24 @@ function Throw({error}) { 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, 9, "onRecoverableError"], [2, 9, "error", 1], [3, 13, "error.cause", 1], [4, 9, "errorInfo"], [5, 14, "componentStack"], [6, 15, "captureOwnerStack()"]] +```js [[1, 9, "onRecoverableError"], [2, 9, "error", 1], [3, 12, "error.cause", 1], [4, 9, "errorInfo"], [5, 13, "componentStack", 15], [6, 14, "captureOwnerStack()"]] // captureOwnerStack is only available in react@canary. -import { captureOwnerStack } from 'react'; -import { hydrateRoot } from 'react-dom/client'; +import {captureOwnerStack} from 'react' +import { hydrateRoot } from "react-dom/client"; +import App from "./App.js"; +import {reportRecoverableError} from "./reportError"; -const root = hydrateRoot( - document.getElementById('root'), - , - { - onRecoverableError: (error, errorInfo) => { - console.error( - 'Caught error', - error, - error.cause, - errorInfo.componentStack, - captureOwnerStack() - ); - } +const container = document.getElementById("root"); +const root = hydrateRoot(container, , { + onRecoverableError: (error, errorInfo) => { + reportRecoverableError({ + error, + cause: error.cause, + componentStack: errorInfo.componentStack, + ownerStack: captureOwnerStack() + }); } -); +}); ``` The onRecoverableError option is a function called with two arguments: From 7570ac6640d6c7dddea4cc4d61445aa00ce0d4f3 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 23 Jan 2025 15:31:07 +0100 Subject: [PATCH 17/20] Remove console patching section It's just a (bad) polyfill for what RDT is doing. Better to lean on RDT for owner stacks in console calls. --- .../reference/react/captureOwnerStack.md | 99 ------------------- 1 file changed, 99 deletions(-) diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index e967019ce58..7eaba4eb5dc 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -386,105 +386,6 @@ export default function App() { -### Expanding error stacks {/*expanding-error-stacks*/} - -In addition to the stack trace of the error itself, you can use `captureOwnerStack` to append the Owner Stack. -This can help trace the error especially when the error is caused by props. The Owner Stack helps trace the flow of props. - - -```js src/index.js [[1, 8, "error.stack"], [2, 7, "captureOwnerStack()"]] -import {captureOwnerStack} from 'react'; -import {createRoot} from 'react-dom/client'; - -const root = createRoot(document.getElementById('root'), { - onUncaughtError: (error, errorInfo) => { - if (process.env.NODE_ENV !== 'production') { - const ownerStack = captureOwnerStack(); - error.stack += ownerStack; - } - - console.error('Uncaught', error); - }, -}).render(); -``` - - - -```js -function useCustomHook() { - throw new Error('Boom!'); -} - -function Component() { - useCustomHook(); -} - -export default function App() { - return ; -} -``` - -```js src/index.js -import {captureOwnerStack} from 'react'; -import {createRoot} from 'react-dom/client'; -import App from './App.js'; -import './styles.css'; - -const root = createRoot(document.getElementById('root'), { - onUncaughtError: (error, errorInfo) => { - if (process.env.NODE_ENV !== 'production') { - const ownerStack = captureOwnerStack(); - error.stack = - // The stack is only split because these sandboxes don't implement - // ignore-listing 3rd party frames via sourcemaps. - // A framework would ignore-list stackframes from React via sourcemaps - // and then you could just `error.stack += ownerStack`. - // To learn more about ignore-listing see https://developer.chrome.com/docs/devtools/x-google-ignore-list - error.stack.split('\n at react-stack-bottom-frame')[0] + ownerStack; - } - - // The stacks are logged instead of showing them in the UI directly to - // highlight that browsers will apply sourcemaps to the logged stacks. - // Note that sourcemapping is only applied in the real browser console not - // in the fake one displayed on this page. - console.error('Uncaught', error); - }, -}).render(); -``` - -```json package.json hidden -{ - "dependencies": { - "react": "experimental", - "react-dom": "experimental", - "react-scripts": "latest" - }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test --env=jsdom", - "eject": "react-scripts eject" - } -} -``` - -```html public/index.html hidden - - - - - - Document - - -

Check the console output.

-
- - -``` - -
- ## Troubleshooting {/*troubleshooting*/} ### The Owner Stack is `null` {/*the-owner-stack-is-null*/} From c76a3d34bad8a26d5b514d09e0666231bf94248e Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Mon, 3 Feb 2025 17:45:49 +0100 Subject: [PATCH 18/20] Rework error overlay illustration root error handlers for prod only captureOwnerStack illustration in patched console.error --- .../reference/react-dom/client/createRoot.md | 817 ++---------------- .../reference/react-dom/client/hydrateRoot.md | 816 ++--------------- 2 files changed, 120 insertions(+), 1513 deletions(-) diff --git a/src/content/reference/react-dom/client/createRoot.md b/src/content/reference/react-dom/client/createRoot.md index 1bcee74298a..0a393394988 100644 --- a/src/content/reference/react-dom/client/createRoot.md +++ b/src/content/reference/react-dom/client/createRoot.md @@ -344,830 +344,127 @@ 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*/} +### Error logging in production {/*error-logging-in-production*/} -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: +By default, React will log all errors to the console. To implement your own error reporting, you can provide the optional error handler root options `onUncaughtError`, `onCaughtError` and `onRecoverableError`: -```js [[1, 8, "onUncaughtError"], [2, 8, "error", 1], [3, 8, "errorInfo"], [4, 12, "componentStack", 15], [5, 13, "captureOwnerStack()"]] -// captureOwnerStack is only available in react@canary. -import { captureOwnerStack } from "react"; +```js [[1, 6, "onCaughtError"], [2, 6, "error", 1], [3, 6, "errorInfo"], [4, 10, "componentStack", 15]] import { createRoot } from "react-dom/client"; -import { reportUncaughtError } from "./reportError"; +import { reportCaughtError } from "./reportError"; const container = document.getElementById("root"); const root = createRoot(container, { - onUncaughtError: (error, errorInfo) => { + onCaughtError: (error, errorInfo) => { if (error.message !== "Known error") { - reportUncaughtError({ + reportCaughtError({ error, componentStack: errorInfo.componentStack, - ownerStack: captureOwnerStack(), }); } }, }); ``` -The onUncaughtError option is a function called with two arguments: +The onCaughtError 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. - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. - -You can use the `onUncaughtError` root option to display error dialogs: +Together with `onUncaughtError` and `onRecoverableError`, you can can implement your own error reporting system: -```html public/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; +```js src/reportError.js +function reportError({ type, error, errorInfo }) { + // The specific implementation is up to you. + // `console.error()` is only used for demonstration purposes. + console.error(type, error, "Component Stack: "); + console.error("Component Stack: ", errorInfo.componentStack); } -.hidden { - display: none; -} -``` - -```js src/reportError.js hidden -function reportError({ title, error, componentStack, ownerStack, 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 errorOwnerStack = document.getElementById("error-owner-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 owner stack - errorOwnerStack.innerText = ownerStack; - - // 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'); +export function onCaughtErrorProd(error, errorInfo) { + if (error.message !== "Known error") { + reportError({ type: "Caught", error, errorInfo }); } - // 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, ownerStack}) { - reportError({ title: "Caught Error", error, componentStack, ownerStack, dismissable: true}); } -export function reportUncaughtError({error, cause, componentStack, ownerStack}) { - reportError({ title: "Uncaught Error", error, componentStack, ownerStack, dismissable: false }); +export function onUncaughtErrorProd(error, errorInfo) { + reportError({ type: "Uncaught", error, errorInfo }); } -export function reportRecoverableError({error, cause, componentStack, ownerStack}) { - reportError({ title: "Recoverable Error", error, componentStack, ownerStack, dismissable: true }); +export function onRecoverableErrorProd(error, errorInfo) { + reportError({ type: "Recoverable", error, errorInfo }); } ``` ```js src/index.js active -// captureOwnerStack is only available in react@canary. -import { captureOwnerStack } from 'react'; import { createRoot } from "react-dom/client"; import App from "./App.js"; -import {reportUncaughtError} from "./reportError"; -import "./styles.css"; +import { + onCaughtErrorProd, + onRecoverableErrorProd, + onUncaughtErrorProd, +} from "./reportError"; const container = document.getElementById("root"); const root = createRoot(container, { - onUncaughtError: (error, errorInfo) => { - if (error.message !== 'Known error') { - reportUncaughtError({ - error, - componentStack: errorInfo.componentStack, - ownerStack: captureOwnerStack() - }); - } - } + // Keep in mind to remove these options in development to leverage + // React's default handlers or implement your own overlay for development. + // The handlers are only specfied unconditionally here for demonstration purposes. + onCaughtError: onCaughtErrorProd, + onRecoverableError: onRecoverableErrorProd, + onUncaughtError: onUncaughtErrorProd, }); root.render(); ``` ```js src/App.js -import { useState } from 'react'; +import { Component, useState } from "react"; -function Component() { - const [throwError, setThrowError] = useState(false); - - if (throwError) { - foo.bar = 'baz'; - } - - return ( - <> - This error shows the error dialog: - - - ); +function Boom() { + foo.bar = "baz"; } -export default function App() { - return ( -
- -
- ); -} -``` +class ErrorBoundary extends Component { + state = { hasError: false }; -```json package.json hidden -{ - "dependencies": { - "react": "experimental", - "react-dom": "experimental", - "react-scripts": "^5.0.0", - "react-error-boundary": "4.0.3" + static getDerivedStateFromError(error) { + return { hasError: true }; } -} -``` - -
- - -### Displaying Error Boundary errors {/*displaying-error-boundary-errors*/} -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, 8, "onCaughtError"], [2, 8, "error", 1], [3, 8, "errorInfo"], [4, 12, "componentStack", 15], [5, 13, "captureOwnerStack()"]] -// captureOwnerStack is only available in react@canary. -import {captureOwnerStack} from 'react'; -import { createRoot } from "react-dom/client"; -import {reportCaughtError} from "./reportError"; - -const container = document.getElementById("root"); -const root = createRoot(container, { - onCaughtError: (error, errorInfo) => { - if (error.message !== 'Known error') { - reportCaughtError({ - error, - componentStack: errorInfo.componentStack, - ownerStack: captureOwnerStack() - }); + render() { + if (this.state.hasError) { + return

Something went wrong.

; } + return this.props.children; } -}); -``` - -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. - - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. - -You can use the `onCaughtError` root option to display error dialogs or filter known errors from logging: - - - -```html public/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, ownerStack, 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 errorOwnerStack = document.getElementById("error-owner-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 owner stack - errorOwnerStack.innerText = ownerStack; - - // 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, ownerStack}) { - reportError({ title: "Caught Error", error, componentStack, ownerStack, dismissable: true}); -} - -export function reportUncaughtError({error, cause, componentStack, ownerStack}) { - reportError({ title: "Uncaught Error", error, componentStack, ownerStack, dismissable: false }); -} - -export function reportRecoverableError({error, cause, componentStack, ownerStack}) { - reportError({ title: "Recoverable Error", error, componentStack, ownerStack, dismissable: true }); -} -``` - -```js src/index.js active -// captureOwnerStack is only available in react@canary. -import {captureOwnerStack} from 'react'; -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, - ownerStack: captureOwnerStack() - }); - } - } -}); -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"); - } + const [triggerUncaughtError, settriggerUncaughtError] = useState(false); + const [triggerCaughtError, setTriggerCaughtError] = useState(false); - function handleKnown() { - setError("known"); - } - return ( <> - { - setError(null); - }} - > - {error != null && } - This error will not show the error dialog: - - This error will show the error dialog: - - - + + {triggerUncaughtError && } + + {triggerCaughtError && ( + + + + )} ); } - -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": "experimental", - "react-dom": "experimental", - "react-scripts": "^5.0.0", - "react-error-boundary": "4.0.3" - } -} ```
-### 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, 8, "onRecoverableError"], [2, 8, "error", 1], [3, 11, "error.cause"], [4, 8, "errorInfo"], [5, 12, "componentStack", 15], [6, 13, "captureOwnerStack()"]] -// captureOwnerStack is only available in react@canary. -import {captureOwnerStack} from 'react' -import { createRoot } from "react-dom/client"; -import {reportRecoverableError} from "./reportError"; - -const container = document.getElementById("root"); -const root = createRoot(container, { - onRecoverableError: (error, errorInfo) => { - reportRecoverableError({ - error, - cause: error.cause, - componentStack: errorInfo.componentStack, - ownerStack: captureOwnerStack(), - }); - } -}); -``` - -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. - - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. - -You can use the `onRecoverableError` root option to display error dialogs: - - - -```html public/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, ownerStack, 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 errorOwnerStack = document.getElementById("error-owner-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 owner stack - errorOwnerStack.innerText = ownerStack; - - // 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, ownerStack}) { - reportError({ title: "Caught Error", error, componentStack, ownerStack, dismissable: true}); -} - -export function reportUncaughtError({error, cause, componentStack, ownerStack}) { - reportError({ title: "Uncaught Error", error, componentStack, ownerStack, dismissable: false }); -} - -export function reportRecoverableError({error, cause, componentStack, ownerStack}) { - reportError({ title: "Recoverable Error", error, componentStack, ownerStack, dismissable: true }); -} -``` - -```js src/index.js active -// captureOwnerStack is only available in react@canary. -import {captureOwnerStack} from 'react' -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, - ownerStack: captureOwnerStack(), - }); - } -}); -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": "experimental", - "react-dom": "experimental", - "react-scripts": "^5.0.0", - "react-error-boundary": "4.0.3" - }, - "main": "/index.js" -} -``` - -
- - ---- ## Troubleshooting {/*troubleshooting*/} ### I've created a root, but nothing is displayed {/*ive-created-a-root-but-nothing-is-displayed*/} diff --git a/src/content/reference/react-dom/client/hydrateRoot.md b/src/content/reference/react-dom/client/hydrateRoot.md index 83d95a82cd8..887cab7d276 100644 --- a/src/content/reference/react-dom/client/hydrateRoot.md +++ b/src/content/reference/react-dom/client/hydrateRoot.md @@ -374,601 +374,124 @@ 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*/} +### Error logging in production {/*error-logging-in-production*/} -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: +By default, React will log all errors to the console. To implement your own error reporting, you can provide the optional error handler root options `onUncaughtError`, `onCaughtError` and `onRecoverableError`: -```js [[1, 9, "onUncaughtError"], [2, 9, "error", 1], [3, 9, "errorInfo"], [4, 13, "componentStack", 15], [5, 14, "captureOwnerStack()"]] -// captureOwnerStack is only available in react@canary. -import { captureOwnerStack } from 'react'; +```js [[1, 6, "onCaughtError"], [2, 6, "error", 1], [3, 6, "errorInfo"], [4, 10, "componentStack", 15]] import { hydrateRoot } from "react-dom/client"; -import App from "./App"; -import {reportUncaughtError} from "./reportError"; +import { reportCaughtError } from "./reportError"; const container = document.getElementById("root"); -hydrateRoot(container, , { - onUncaughtError: (error, errorInfo) => { - if (error.message !== 'Known error') { - reportUncaughtError({ +const root = hydrateRoot(container, { + onCaughtError: (error, errorInfo) => { + if (error.message !== "Known error") { + reportCaughtError({ error, componentStack: errorInfo.componentStack, - ownerStack: captureOwnerStack() }); } - } + }, }); ``` -The onUncaughtError option is a function called with two arguments: +The onCaughtError 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. - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. - -You can use the `onUncaughtError` root option to display error dialogs: +Together with `onUncaughtError` and `onRecoverableError`, you can can implement your own error reporting system: -```html public/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 +function reportError({ type, error, errorInfo }) { + // The specific implementation is up to you. + // `console.error()` is only used for demonstration purposes. + console.error(type, error, "Component Stack: "); + console.error("Component Stack: ", errorInfo.componentStack); } -``` -```js src/reportError.js hidden -function reportError({ title, error, componentStack, ownerStack, 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 errorOwnerStack = document.getElementById("error-owner-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 = ''; +export function onCaughtErrorProd(error, errorInfo) { + if (error.message !== "Known error") { + reportError({ type: "Caught", error, errorInfo }); } - - // Display component stack - errorComponentStack.innerText = componentStack; - - // Display owner stack - errorOwnerStack.innerText = ownerStack; - - // 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, ownerStack}) { - reportError({ title: "Caught Error", error, componentStack, ownerStack, dismissable: true}); } -export function reportUncaughtError({error, cause, componentStack, ownerStack}) { - reportError({ title: "Uncaught Error", error, componentStack, ownerStack, dismissable: false }); +export function onUncaughtErrorProd(error, errorInfo) { + reportError({ type: "Uncaught", error, errorInfo }); } -export function reportRecoverableError({error, cause, componentStack, ownerStack}) { - reportError({ title: "Recoverable Error", error, componentStack, ownerStack, ownerStack, dismissable: true }); +export function onRecoverableErrorProd(error, errorInfo) { + reportError({ type: "Recoverable", error, errorInfo }); } ``` ```js src/index.js active -// captureOwnerStack is only available in react@canary. -import { captureOwnerStack } from 'react'; import { hydrateRoot } from "react-dom/client"; -import App from "./App"; -import {reportUncaughtError} from "./reportError"; -import "./styles.css"; -import {renderToString} from 'react-dom/server'; +import App from "./App.js"; +import { + onCaughtErrorProd, + onRecoverableErrorProd, + onUncaughtErrorProd, +} from "./reportError"; const container = document.getElementById("root"); hydrateRoot(container, , { - onUncaughtError: (error, errorInfo) => { - if (error.message !== 'Known error') { - reportUncaughtError({ - error, - componentStack: errorInfo.componentStack, - ownerStack: captureOwnerStack() - }); - } - } + // Keep in mind to remove these options in development to leverage + // React's default handlers or implement your own overlay for development. + // The handlers are only specfied unconditionally here for demonstration purposes. + onCaughtError: onCaughtErrorProd, + onRecoverableError: onRecoverableErrorProd, + onUncaughtError: onUncaughtErrorProd, }); ``` ```js src/App.js -import { useState } from 'react'; +import { Component, useState } from "react"; -function Component() { - const [throwError, setThrowError] = useState(false); - - if (throwError) { - foo.bar = 'baz'; - } - - return ( - <> - This error shows the error dialog: - - - ); +function Boom() { + foo.bar = "baz"; } -export default function App() { - return ( -
- -
- ); -} -``` +class ErrorBoundary extends Component { + state = { hasError: false }; -```json package.json hidden -{ - "dependencies": { - "react": "experimental", - "react-dom": "experimental", - "react-scripts": "^5.0.0", - "react-error-boundary": "4.0.3" + static getDerivedStateFromError(error) { + return { hasError: true }; } -} -``` - -
- - -### Displaying Error Boundary errors {/*displaying-error-boundary-errors*/} - -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, 9, "onCaughtError"], [2, 9, "error", 1], [3, 9, "errorInfo"], [4, 13, "componentStack", 15], [5, 14, "captureOwnerStack()"]] -// captureOwnerStack is only available in react@canary. -import {captureOwnerStack} from 'react'; -import { hydrateRoot } from "react-dom/client"; -import App from "./App.js"; -import {reportCaughtError} from "./reportError"; -const container = document.getElementById("root"); -hydrateRoot(container, , { - onCaughtError: (error, errorInfo) => { - if (error.message !== 'Known error') { - reportCaughtError({ - error, - componentStack: errorInfo.componentStack, - ownerStack: captureOwnerStack() - }); + render() { + if (this.state.hasError) { + return

Something went wrong.

; } + return this.props.children; } -}); -``` - -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. - - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. - -You can use the `onCaughtError` root option to display error dialogs or filter known errors from logging: - - - -```html public/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, ownerStack, 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 errorOwnerStack = document.getElementById("error-owner-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 owner stack - errorOwnerStack.innerText = ownerStack; - - // 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, ownerStack}) { - reportError({ title: "Caught Error", error, componentStack, ownerStack, dismissable: true}); -} - -export function reportUncaughtError({error, cause, componentStack}) { - reportError({ title: "Uncaught Error", error, componentStack, ownerStack, dismissable: false }); -} - -export function reportRecoverableError({error, cause, componentStack}) { - reportError({ title: "Recoverable Error", error, componentStack, ownerStack, ownerStack, dismissable: true }); } -``` - -```js src/index.js active -// captureOwnerStack is only available in react@canary. -import {captureOwnerStack} from 'react'; -import { hydrateRoot } from "react-dom/client"; -import App from "./App.js"; -import {reportCaughtError} from "./reportError"; -import "./styles.css"; - -const container = document.getElementById("root"); -hydrateRoot(container, , { - onCaughtError: (error, errorInfo) => { - if (error.message !== 'Known error') { - reportCaughtError({ - error, - componentStack: errorInfo.componentStack, - ownerStack: captureOwnerStack() - }); - } - } -}); -``` - -```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"); - } + const [triggerUncaughtError, settriggerUncaughtError] = useState(false); + const [triggerCaughtError, setTriggerCaughtError] = useState(false); - function handleKnown() { - setError("known"); - } - return ( <> - { - setError(null); - }} - > - {error != null && } - This error will not show the error dialog: - - This error will show the error dialog: - - - + + {triggerUncaughtError && } + + {triggerCaughtError && ( + + + + )} ); } - -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": "experimental", - "react-dom": "experimental", - "react-scripts": "^5.0.0", - "react-error-boundary": "4.0.3" - } -} ``` -
- -### 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, 9, "onRecoverableError"], [2, 9, "error", 1], [3, 12, "error.cause", 1], [4, 9, "errorInfo"], [5, 13, "componentStack", 15], [6, 14, "captureOwnerStack()"]] -// captureOwnerStack is only available in react@canary. -import {captureOwnerStack} from 'react' -import { hydrateRoot } from "react-dom/client"; -import App from "./App.js"; -import {reportRecoverableError} from "./reportError"; - -const container = document.getElementById("root"); -const root = hydrateRoot(container, , { - onRecoverableError: (error, errorInfo) => { - reportRecoverableError({ - error, - cause: error.cause, - componentStack: errorInfo.componentStack, - ownerStack: captureOwnerStack() - }); - } -}); -``` - -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. - - With [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development. - -You can use the `onRecoverableError` root option to display error dialogs for hydration mismatches: - - - ```html public/index.html hidden @@ -977,225 +500,12 @@ You can use the `onRecoverableError` root option to display error dialogs for hy - - -
Server
+
Server content before hydration.
``` - -```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, ownerStack, 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 errorOwnerStack = document.getElementById("error-owner-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 owner stack - errorOwnerStack.innerText = ownerStack; - - // 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, ownerStack}) { - reportError({ title: "Caught Error", error, componentStack, ownerStack, dismissable: true}); -} - -export function reportUncaughtError({error, cause, componentStack, ownerStack}) { - reportError({ title: "Uncaught Error", error, componentStack, ownerStack, dismissable: false }); -} - -export function reportRecoverableError({error, cause, componentStack, ownerStack}) { - reportError({ title: "Recoverable Error", error, componentStack, ownerStack, dismissable: true }); -} -``` - -```js src/index.js active -// captureOwnerStack is only available in react@canary. -import {captureOwnerStack} from 'react' -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, - ownerStack: captureOwnerStack() - }); - } -}); -``` - -```js src/App.js -import { useState } from 'react'; -import { ErrorBoundary } from "react-error-boundary"; - -export default function App() { - 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": "experimental", - "react-dom": "experimental", - "react-scripts": "^5.0.0", - "react-error-boundary": "4.0.3" - }, - "main": "/index.js" -} -``` -
## Troubleshooting {/*troubleshooting*/} From 053bd5c1d837265411fef7d71feb34847163fb0d Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Tue, 11 Feb 2025 01:54:52 +0100 Subject: [PATCH 19/20] Clarify returntype --- .../reference/react/captureOwnerStack.md | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/content/reference/react/captureOwnerStack.md b/src/content/reference/react/captureOwnerStack.md index 7eaba4eb5dc..ac899496b88 100644 --- a/src/content/reference/react/captureOwnerStack.md +++ b/src/content/reference/react/captureOwnerStack.md @@ -29,11 +29,11 @@ const stack = captureOwnerStack(); Call `captureOwnerStack` to get the current Owner Stack. ```js {5,5} -import {captureOwnerStack} from 'react'; +import * as React from 'react'; function Component() { if (process.env.NODE_ENV !== 'production') { - const ownerStack = captureOwnerStack(); + const ownerStack = React.captureOwnerStack(); console.log(ownerStack); } } @@ -53,7 +53,7 @@ Owner Stacks are available in - React's event handlers (e.g. `