This is an amazing combination of features that is widely used in the React source code.

When I read the source code and saw here, the mood experienced:

Muddled — confused — meditating — looking into a document — suddenly open

After reading this article, I believe you will also sigh:

Can you still do this?

The origin of

We know that There is an Error Boundary in React, which helps us display the “Error status” of a component when an Error occurs.

In order to implement this feature, errors must be caught.

So in React source code, all user code is wrapped up in a method for execution.

Something like this:

function wrapper(func) {
  try {
    func();
  } catch(e) {
    / /... Handling errors}}Copy the code

For example, when triggering componentDidMount:

wrapper(componentDidMount);
Copy the code

Everything was perfect, but React, as a world-class front-end framework, has a wide audience and strives for perfection in everything.

This is not, someone mentioned the issue:

Executing user code in a try catch like this invalidates Pause on Exceptions in the browser debugger.

Pause on Exceptions

What is Pause on Exceptions?

It is a feature of the Source panel of the browser debugging tool.

When this feature is enabled, code that throws an error is automatically stopped at that line at runtime, as if a breakpoint had been struck at that line.

For example, execute the following code and enable this function:

let a = c;
Copy the code

Execution of the code is paused at this line.

This feature is a handy way to find out where uncaught errors occur.

However, when React wraps user code behind a try catch, even if the code throws an error, it gets caught.

Cannot Pause on user code that throws an error because the error has been caught by React.

Unless we further open Pause on caught exceptions.

Enabling this feature causes the code to pause at the location where the caught error occurred.

How to solve

The code I wrote in componentDidMount does not catch errors, but Pause on exceptions fail when errors occur, which can be a little confusing to the user.

So, in production, React continues to implement wrappers using try catches.

In the development environment, for better debugging experience, we need to implement a try catch mechanism, including the following features:

  • Catch errors thrown by user code and enable the Error Boundary function to function properly

  • Does not catch errors thrown by user code, and does not invalidate Pause on Exceptions

How does React implement this seemingly contradictory feature?

How do I “catch” errors

Let’s do the first: catch errors thrown by user code.

However, you cannot use a try catch, because that would invalidate Pause on Exceptions.

The solution is to listen for window error events.

According to the GlobalEventHandlers onerror MDN, it can listen to the two types of error:

  • Js runtime errors (including syntax errors). Window raises an error event on the ErrorEvent interface

  • Failed to load resources such as or

Implement wrapperDev for the development environment:

// Development environment wrapper
function wrapperDev(func) {
  function handleWindowError(error) {
    // Collect errors and pass them to the Error Boundary
  }

  window.addEventListener('error', handleWindowError);
  func();
  window.removeEventListener('error', handleWindowError);
}
Copy the code

When func throws an error, it is handled by handleWindowError.

However, in production wrapperPrd, errors thrown by func are caught and do not affect subsequent code execution.

function wrapperPrd(func) {
  try {
    func();
  } catch(e) {
    / /... Handling errors}}Copy the code

If an error is thrown in the development environment FUNc, the execution of the code will be interrupted.

If you execute the following code, finish will be printed.

wrapperPrd(() = > {throw Error(123)})
console.log('finish');
Copy the code

However, when the following code is executed, the code execution is interrupted and finish is not printed.

wrapperDev(() = > {throw Error(123)})
console.log('finish');
Copy the code

How do you keep the execution of subsequent code uninterrupted without catching errors thrown by user code?

How to make code execution uninterrupted

The answer is to invoke the user code in the event callback that is triggered by dispatchEvent.

Based on the EventTarget. DispatchEvent MDN:

Unlike events fired by DOM nodes (such as click events), callbacks are fired asynchronously by the Event Loop.

Events that are triggered by dispatchEvent are triggered synchronously, and errors thrown in event callbacks do not affect dispatchEvent callers.

Let’s move on to wrapperDev.

First create a fictional DOM node, event object, and fictitious event type:

// Create a fictional DOM node
const fakeNode = document.createElement('fake');
/ / create the event
const event = document.createEvent('Event');
// Create a fictitious event type
const evtType = 'fake-event';
Copy the code

Initialize the event object and listen for events. The user code is invoked in the event callback. Trigger event:

function callCallback() {
  fakeNode.removeEventListener(evtType, callCallback, false); 
  func();
}

// Listen for fictitious event types
fakeNode.addEventListener(evtType, callCallback, false);

// Initialize the event
event.initEvent(evtType, false.false);

// Triggers the event
fakeNode.dispatchEvent(event);
Copy the code

The complete process is as follows:

function wrapperDev(func) {
  function handleWindowError(error) {
    // Collect errors and pass them to the Error Boundary
  }
  
  function callCallback() {
    fakeNode.removeEventListener(evtType, callCallback, false); 
    func();
  }
  
  const event = document.createEvent('Event');
  const fakeNode = document.createElement('fake');
  const evtType = 'fake-event';

  window.addEventListener('error', handleWindowError);
  fakeNode.addEventListener(evtType, callCallback, false);

  event.initEvent(evtType, false.false);
  

  fakeNode.dispatchEvent(event);
  
  window.removeEventListener('error', handleWindowError);
}
Copy the code

When we call:

wrapperDev(() = > {throw Error(123)})
Copy the code

Will be executed in sequence:

  1. DispatchEvent Triggers the event callback callCallback

  2. A throw Error(123) is executed inside the callCallback to throw the Error

  3. The callCallback execution interrupts, but the function that called it continues.

  4. Error(123) is captured by the Window Error handler for use in the Error Boundary

Step 2 makes Pause on Exceptions not invalid.

Steps 3 and 4 enable the error to be caught without blocking subsequent code execution, simulating the effect of a try catch.

conclusion

I have to say, the React wave is very delicate.

Our mini Wrapper implementation has many disadvantages, such as:

  • There is no compatibility for different browsers

  • Not considering that other code also fires the Window Error Handler

See here for a full wrapper to the React source