The implementation of Error Boundaries is quite ingenious

Hello, I'm Carson.

This article will explain the complete implementation logic of Error Boundaries in React.

A picture summarizes:

Here is a brief explanation of React workflow, which is useful later. There are three steps:

  1. Trigger update
  2. render phase: calculate the side effects caused by update
  3. commit phase: perform side effects in the host environment

There are many side effects, such as:

  • Insert DOM node
  • Execute useEffect callback

All right, let's get to the subject.

Welcome to join Human high quality front-end framework group , with flying

What is Error Boundaries

React provides two API s related to error handling:

  • getDerivedStateFromError: a static method that provides an opportunity to render the fallback UI when an error occurs
  • componentDidCatch: a component instance method that provides an opportunity to record error information when an error occurs

Classcomponents that use these two API s are often called Error Boundaries.

All errors in the React workflow that occur in the descendant component of Error Boundaries will be captured by Error Boundaries.

From the introduction at the beginning, we can know that React workflow refers to:

  • render stage
  • commit phase

Consider the following codes:

class ErrorBoundary extends Component {
  componentDidCatch(e) {
    console.warn(""An error occurred", e);
  }
  render() {
    return <div>{this.props.children}</div>;
  }
}

const App = () => (
    <ErrorBoundary>
    <A><B/></A>
    <C/>
    <ErrorBoundary>
)

A. As descendants of ErrorBoundary, B and C will be captured by the componentDidCatch method in ErrorBoundary when errors occur in the React workflow.

Step 1: catch errors

First, let's look at when errors in workflow are captured.

The core code of the render phase is as follows. The errors will be handled by handleError:

do {
  try {
    // For concurrent updates, workLoopConcurrent
workLoopSync();
    break;
  } catch (thrownValue) {
    handleError(root, thrownValue);
  }
} while (true);

The commit phase involves a lot of work, such as:

  • componentDidMount/Update execution
  • Bind / unbind ref
  • Useeffect / uselayouteeffect callback and destroy execution

These tasks will be executed in the following form, and the errors will be handled by captureCommitPhaseError:

try {
// ... perform a job 
} catch (error) {
  captureCommitPhaseError(fiber, fiber.return, error);
}

Step 2: construct callback

It can be found that even without Error Boundaries, the errors in the workflow have been captured by React. The correct logic should be:

  • If there are Error Boundaries, execute the corresponding API
  • Throw the prompt message of React
  • If there are no Error Boundaries, an uncapped error is thrown

Therefore, no matter handleError or captureCommitPhaseError, it will start from the parent node of the node where the error occurs, traverse up layer by layer to find the nearest Error Boundaries.

Once found, it constructs:

  • callback used to execute the Error Boundaries API
  • callback used to throw the React prompt

  // ... logic is pruned for readability
function createClassErrorUpdate() {
  if (typeof getDerivedStateFromError === 'function') {
// callback to execute getDerivedStateFromError
    update.payload = () => {
      return getDerivedStateFromError(error);
};
// callback used to throw the React prompt
    update.callback = () => {
      logCapturedError(fiber, errorInfo);
    };
  }
  if (inst !== null && typeof inst.componentDidCatch === 'function') {
// callback for executing componentDidCatch
    update.callback = function callback() {
      this.componentDidCatch(error);
    };
  }
  return update;
}

If no Error Boundaries are found, continue to traverse up to the root node.

The following is constructed:

  • callback used to throw an uncapped error
  • callback used to throw the React prompt
// ... logic is pruned for readability
funffction createRootErrorUpdate() {
  // callback used to throw "uncapped error" and "prompt of React"
  update.callback = () => {
    onUncaughtError(error);
    logCapturedError(fiber, errorInfo);
  };
  return update;
}

Execute callback

When does the constructed callback execute?

In React, there are two API s to execute user-defined callback:

  • For ClassComponent, both the new state and callback parameters in this. Setstate (new state, callback) can pass Function as callback

Therefore, for Error Boundaries, it is equivalent to actively triggering an update:

this.setState(() => {
  // callback to execute getDerivedStateFromError
}, () => {
  // callback for executing componentDidCatch
  //  And a callback for throwing the React prompt
})
  • For the root node, execute the callback parameter in ReactDOM.render(element, container, callback) to pass Function as callback

Therefore, when there are no Error Boundaries, it is equivalent to actively executing the following functions:

ReactDOM.render(element, container, () => {
// callback used to throw "uncapped error" and "prompt of React"
})

Therefore, the implementation of Error Boundaries can be regarded as a new function implemented by React using the existing API.

summary

People often ask: why do Hooks have no Error Boundaries?

It can be seen that the implementation of Error Boundaries makes use of the feature that this.setState can pass callback, and useState cannot be fully benchmarked for the time being.

Finally, I'll leave you an assignment in Official documents The errors described in four cases will not be captured by Error Boundaries.

Using this knowledge, can you analyze why they are not captured?

Keywords: Javascript Front-end React

Added by Sayian on Fri, 03 Dec 2021 08:37:08 +0200