Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reference docs for experimental captureOwnerStack #7427

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

eps1lon
Copy link
Collaborator

@eps1lon eps1lon commented Jan 13, 2025

Based on #7430

Preview:
captureOwnerStack
createRoot error handlers
hydrateRoot error handlers

The owner stack vs parent section probably deserves its own page since owner stacks also appear in devtools and console.createTask.

Should I just move this into a "debugging" section inside "learn"?

Copy link

vercel bot commented Jan 13, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
19-react-dev ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 14, 2025 4:38pm
react-dev ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 14, 2025 4:38pm

Copy link

github-actions bot commented Jan 13, 2025

Size changes

📦 Next.js Bundle Analysis for react-dev

This analysis was generated by the Next.js Bundle Analysis action. 🤖

⚠️ Global Bundle Size Increased

Page Size (compressed)
global 110.38 KB (🟡 +2 B)
Details

The global bundle is the javascript bundle that loads alongside every page. It is in its own category because its impact is much higher - an increase to its size means that every page on your website loads slower, and a decrease means every page loads faster.

Any third party scripts you have added directly to your app using the <script> tag are not accounted for in this analysis

If you want further insight into what is behind the changes, give @next/bundle-analyzer a try!

Five Pages Changed Size

The following pages changed size from the code in this PR compared to its base branch:

Page Size (compressed) First Load
/404 125.09 KB (🟡 +18 B) 235.47 KB
/500 125.09 KB (🟡 +18 B) 235.48 KB
/[[...markdownPath]] 126.97 KB (🟡 +34 B) 237.35 KB
/errors 125.37 KB (🟡 +18 B) 235.75 KB
/errors/[errorCode] 125.34 KB (🟡 +18 B) 235.72 KB
Details

Only the gzipped size is provided here based on an expert tip.

First Load is the size of the global bundle plus the bundle for the individual page. If a user were to show up to your website and land on a given page, the first load size represents the amount of javascript that user would need to download. If next/link is used, subsequent page loads would only need to download that page's bundle (the number in the "Size" column), since the global bundle has already been downloaded.

Any third party scripts you have added directly to your app using the <script> tag are not accounted for in this analysis

Next to the size is how much the size has increased or decreased compared with the base branch of this PR. If this percentage has increased by 10% or more, there will be a red status indicator applied, indicating that special attention should be given to this.

Copy link
Member

@rickhanlonii rickhanlonii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the naming we should go with is:

  • Component Stack: what you're calling "parent component stack" here
  • Owner Stack: what you're calling "owner component stack" here

* **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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a "parent Component Stack" isn't that just the Component Stack? Adding "parent" makes it sound like it doesn't include the component that errored.

This page should also add a usage item for calling captureOwnerStack to get the owner stack.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like expanding the existing usages is sufficient. Is that what you meant? Adding a whole new section feels like overkill.

src/content/reference/react/captureOwnerStack.md Outdated Show resolved Hide resolved
src/content/reference/react-dom/client/createRoot.md Outdated Show resolved Hide resolved
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*/}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's wrap this in a <DeepDive />

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

src/content/reference/react/captureOwnerStack.md Outdated Show resolved Hide resolved
src/content/reference/react/captureOwnerStack.md Outdated Show resolved Hide resolved
onCaughtError: (error, errorInfo) => {
if (process.env.NODE_ENV !== 'production') {
const ownerStack = captureOwnerStack();
error.stack += ownerStack;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really recommend appending it to the existing stack? Won't that add it to the end?

IMO this should be like:

if (__DEV__) { 
  showDialog(error, ownerStack, componentStack);
} else {
  logProdError(error, componentStack);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do that in Next.js and it's conceptually how you arrive at the error if every JSX callsite would just be a function call.


<Intro>

`captureOwnerStack` reads the current **owner** Component stack and returns it as a string if available.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make it more clear that this is dev-only. I would mention it here, in a Caveat, and a Troubleshooting "My owner stacks are empty in production" section.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did all that. Also explained this in the createRoot and hydrateRoot usage examples. Though possible I missed some spots.

@sophiebits
Copy link
Member

Aren't we getting rid of parent stacks entirely eventually? I thought the plan was for owner stacks to subsume them. In which case I might still discuss which stack frames you'll see but not emphasize the difference so much.

@sebmarkbage
Copy link
Contributor

sebmarkbage commented Jan 13, 2025

Aren't we getting rid of parent stacks entirely eventually? I thought the plan was for owner stacks to subsume them.

Not for production logging. They're the ones passed to the deriveStateFromError/componentDidCatch and onError on the server. They only go away in the standard DEV console.error reporting.

@rickhanlonii
Copy link
Member

There should also be runable sandboxes for the Usage examples

@gaearon
Copy link
Member

gaearon commented Jan 14, 2025

There should also be runable sandboxes for the Usage examples

To expand on that — it’s not just for it to be nice but because it’s a proof the API actually works end to end and does what the docs page says it does. We’ve had cases where that wasn’t the case and the Usage examples helped uncover the misunderstandings.

eps1lon and others added 9 commits January 14, 2025 16:49
Forks on CodeSandbox will ignore `/index.html` and create their own `public/index.html` which leads to broken sandboxes in CodeSandbox.

We now throw if `/index.html` is used and only use `public/index.html` which works both in Sandpack and CodeSandbox.

This also means we're consistent. Some sandboxes were already using the functioning `public/index.html`.
parent Component stack -> Component Stack
owner Component stack -> Owner Stack
Was just rephrasing what we already had and gave wrong impression
i.e. everywhere I found mention of `"componentStack"`

<Wip>

**This API is experimental and is not available in a stable version of React yet.**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should update this to the Canary badge, and just wait until we add this to Canary to merge the docs.

Comment on lines +56 to +57
If no Owner Stack is available, it returns an empty string.
Outside of development builds, `null` is returned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If no Owner Stack is available, it returns an empty string.
Outside of development builds, `null` is returned.
If no Owner Stack is available, it returns an empty string. Outside of development builds, `null` is returned.

Also, instead of "if no owner stack is available" we should say when owner stacks are not available. Like maybe not too specific, but something like "if it's not called inside a component lifecycle".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We call this out in the troubleshooting section so we should just also include that explanation here.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would shorten the line length of this comment so you don't force a hscroll on desktop at least:

Screenshot 2025-01-19 at 10 42 53 AM

Comment on lines +117 to +118
console.log(errorInfo.componentStack);
console.log(captureOwnerStack());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would add labels:

Suggested change
console.log(errorInfo.componentStack);
console.log(captureOwnerStack());
console.log('Component Stack:', errorInfo.componentStack);
console.log('Owner Stack:', captureOwnerStack());
Screenshot 2025-01-19 at 10 46 52 AM

Comment on lines +159 to +160
`SubComponent` would throw an error.
The Component Stack of that error would be
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen this in a few places - think there's way too many newlines in these docs, instead I would write it more paragraph style:

Suggested change
`SubComponent` would throw an error.
The Component Stack of that error would be
`SubComponent` would throw an error. The Component Stack of that error would be:

But you should also expand here, because it's not clear what SubComponent is:

Suggested change
`SubComponent` would throw an error.
The Component Stack of that error would be
In this example, `SubComponent` throws an error in render, which is not caught by an error boundary. The root `onUncaughtError` method is called, and logs the Component Stack and Owner Stack. The Component Stack of that error is:


## Usage {/*usage*/}

### Improve error reporting {/*improve-error-reporting*/}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I read this and the usage below, I hear "reporting for production logging" which isn't the case. I really think the usages here are limited to just instrumenting DEV dialogs.

onUncaughtError: (error, errorInfo) => {
if (process.env.NODE_ENV !== 'production') {
const ownerStack = captureOwnerStack();
error.stack += ownerStack;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you do this? Isn't it already in the stack if you have DevTools open? And if DevTools is open, won't this double add the owner 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a separate usage section like "How to collapse frames in DEV dialogs without sourcemaps"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe Troubleshooting

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also seems weird to me because React DevTools adds the owner stack in the stack trace? Why would you want to do this?

Comment on lines +295 to +296
`captureOwnerStack` was called outside of development builds.
For performance reasons, React will not keep track of Owners in production.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`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 Owner Stacks in production.

Comment on lines +300 to +301
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

And can we add a sandbox with the issue and fix?

Copy link
Member

@rickhanlonii rickhanlonii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other comments are nits, but I think there's a usage section still missing here for patching console.error. If you only instrument owner stacks for DEV dialogs in the root error handlers, you will miss owner stacks in console.error calls. So you need to patch console.error, but you need to patch it after dev tools, which is tricky to get right and needs docs.


// 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);
Copy link
Member

@rickhanlonii rickhanlonii Jan 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you've patched console.error to add owner stacks, and you call console.error here, it will result in multiple owner stacks being applied. It's not clear to me whether you should only patch console.error, or if you should instrument the root handlers and console.error without them overlapping somehow.

@sebmarkbage how should this work ideally? Maybe we're over-emphasizing the root handler case?

The way I expected this to work is like:

onUncaughtError() {
  if (process.env.NODE_ENV !== 'production') {
      const ownerStack = captureOwnerStack();
      sendDevOnlyLogs(error, ownerStack);
  } else {
    sendProductionLogs(error);
  }
  
  // Patch or DevTools will add logs to the console output.
  console.error(error);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants