[source code analysis] may be the most practical solution for react native exception in the whole network [suggest collection]

Time:2021-5-5

preface

When doing react native hybrid development, the production environment will sometimes encounter the situation of turning on the RN application white screen, flashing back to the native page or directly causing app crash. Through the analysis of APP logs, it is found that the reasons can be classified into the following two types:

  1. JS layer compile run error. Generally, some special data or scenarios cause JS execution to report errors;
  2. Exception occurs when JS translates native UI or communicates with native modules

For the first point, we can quickly trace and solve the problem JS code through the log, but for the second point, it is often the execution of the underlying code of the framework that blocks the UI rendering, and the error log information cannot locate the problem, such as:

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: com.facebook.react.common.c: Error: JS Functions are not convertible to dynamic
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: 
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: This error is located at:
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in u
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in Tile
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in Tile
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in TouchableWithoutFeedback
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in Unknown
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in h
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTScrollView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in u
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in v
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in f
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in h
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in AndroidHorizontalScrollContentView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in AndroidHorizontalScrollView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in u
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in v
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in f
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in n
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in inject-with-store(n)
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in MobXProvider
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in I
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in c, stack:
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: <unknown>@-1
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:2227
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: <unknown>@19:1668
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:62783
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:66674
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:69555
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: <unknown>@89:81296
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:3238
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:81253
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:81007
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:80310
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:79323
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:68624
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:21420
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:657
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:2816
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:3311
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: <unknown>@28:822
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:2565
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]:794
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: [email protected]
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     at com.facebook.react.modules.core.ExceptionsManagerModule.showOrThrowError(ExceptionsManagerModule.java:54)
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     at com.facebook.react.modules.core.ExceptionsManagerModule.reportFatalException(ExceptionsManagerModule.java:38)
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     at java.lang.reflect.Method.invoke(Native Method)
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:372)
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:158)
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     at android.os.Handler.handleCallback(Handler.java:907)
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     at android.os.Handler.dispatchMessage(Handler.java:105)
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:29)
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     at android.os.Looper.loop(Looper.java:216)
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:232)
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     at java.lang.Thread.run(Thread.java:784)

The application exception is not the worst. The worst is that it brings users a bad experience, although the actual probability is very low.
When there is an exception, we should prompt and comfort users by demoting the UI (such as the common 404 page on the web side and the pop-up window of “network lost, please try again later”), and guide users to turn to the normal page.
Unfortunately, we usually don’t have the initiative now. All exception handling is done by react native framework itself. Therefore, we need to take over the exception handling power from react native to implement our own logic (similar toReverse control reverseThoughts)

Next, will lead you step by step analysis and implementation.

Analysis of red / yellow screen prompt of react native

No matter what causes the abnormal RN application, theDevelopment modeEnvironment (inRelease / productionBy default, it will be prompted in red box or yellow box

Please note that in this article, both error and warning are regarded as exceptions

Red screen:
[source code analysis] may be the most practical solution for react native exception in the whole network [suggest collection]
Yellow screen:
[source code analysis] may be the most practical solution for react native exception in the whole network [suggest collection]

In the official description:

###Red screen error

The error report in the application will be displayed in full screen red in the application (debugging mode), which is called red box error report. You can use 'console. Error()' to trigger the red screen error manually.

###Yellow screen warning

The warning in the application will be displayed in full screen yellow in the application (debugging mode), which is called yellow box error. Click on the warning to see the details or ignore them. Similar to the red alarm, you can use 'console. Warn()' to trigger the yellow alarm manually.

These two full screen prompts are react native’s handling of RN application exceptions.
So here’s the idea. We just need to find the red and yellow screen of Rn and replace it with our own business logic
The schematic diagram is as follows:
[source code analysis] may be the most practical solution for react native exception in the whole network [suggest collection]

OK, next we need to find this entry from the source code. Don’t be afraid of the source code. Follow my idea, let’s go!

Find the entry from the source code

1. Find the red screen entry point

In the above red screen picture, we canconsole.error('I am red box')The red screen prompt is triggered. The error stack trace information is printed in the prompt

console.error: "I am red box"
error
    
<unknown>
    C:\workspace\test_timer_picker\node_modules\react-native\Libraries\Renderer\oss\ReactFabric-prod.js:6808:9
_callTimer
    C:\workspace\test_timer_picker\node_modules\react-native\Libraries\Renderer\oss\ReactNativeRenderer-dev.js:8778:10
callTimers
    C:\workspace\test_timer_picker\node_modules\react-native\Libraries\Renderer\oss\ReactNativeRenderer-dev.js:9080:8
__callFunction
    
<unknown>
    
__guard
    C:\workspace\test_timer_picker\node_modules\react-native\Libraries\ART\ReactNativeART.js:169:9
callFunctionReturnFlushedQueue
    
callFunctionReturnFlushedQueue
    [native code]

The location of the file where the error occurred is indicated

\node_modules\react-native\Libraries\Renderer\oss\ReactFabric-prod.js
\node_modules\react-native\Libraries\Renderer\oss\ReactNativeRenderer-dev.js
\node_modules\react-native\Libraries\ART\ReactNativeART.js

Query in these files in turnconsole.error, which can beReactNativeRenderer-dev.jsIn fileshowErrorDialogMethod

  ExceptionsManager.handleException(errorToHandle, false);
  // Return false here to prevent ReactFiberErrorLogger default behavior of
  // logging error details to console.error. Calls to console.error are
  // automatically routed to the native redbox controller, which we've already
  // done above by calling ExceptionsManager.

Calling console. Error will automatically navigate to native red screen controller, and then checkshowErrorDialogNote for method:

/**
 * Intercept lifecycle errors and ensure they are shown with the correct stack
 * trace within the native redbox component.
 */
function showErrorDialog(capturedError) {/****/}

Intercept lifecycle errors and make sure that the correct stack trace is displayed in the native Redbox component
Perfect, we find the reason of the red screen according to the error stack information!
Take a closer look at this note:

  //Calls to console.error are
  // automatically routed to the native redbox controller, which we've already
  // done above by calling ExceptionsManager.

“Calling console. Error will automatically navigate to native red screen controller because we have already called exceptionsmanager”

So at this point, we can think of,Generate red screen = = = because of what exceptions manager didWhat we need to do is to replace the logic implemented by exceptions manager with our own!

Tips: carefully look for the source codeshowErrorDialog()Where is called, you will find itlogCapturedError()And higher uplogError(), analysislogError()You will find that the originalError boundaryThe ability to capture component rendering errors is also related to it

OK, continue to see exceptionsmanager.js. Its path is:node_modules\react-native\Libraries\Core\ExceptionsManager.jsThe contents are as follows:

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @flow
 */

'use strict';

import type {ExtendedError} from 'parseErrorStack';

/**
 * Handles the developer-visible aspect of errors and exceptions
 */
let exceptionID = 0;
function reportException(e: ExtendedError, isFatal: boolean) {
  const {ExceptionsManager} = require('NativeModules');
  if (ExceptionsManager) {
    const parseErrorStack = require('parseErrorStack');
    const stack = parseErrorStack(e);
    const currentExceptionID = ++exceptionID;
    const message =
      e.jsEngine == null ? e.message : `${e.message}, js engine: ${e.jsEngine}`;
    if (isFatal) {
      ExceptionsManager.reportFatalException(
        message,
        stack,
        currentExceptionID,
      );
    } else {
      ExceptionsManager.reportSoftException(message, stack, currentExceptionID);
    }
    if (__DEV__) {
      const symbolicateStackTrace = require('symbolicateStackTrace');
      symbolicateStackTrace(stack)
        .then(prettyStack => {
          if (prettyStack) {
            ExceptionsManager.updateExceptionMessage(
              e.message,
              prettyStack,
              currentExceptionID,
            );
          } else {
            throw new Error('The stack is null');
          }
        })
        .catch(error =>
          console.warn('Unable to symbolicate stack trace: ' + error.message),
        );
    }
  }
}

declare var console: typeof console & {
  _errorOriginal: Function,
  reportErrorsAsExceptions: boolean,
};

/**
 * Logs exceptions to the (native) console and displays them
 */
function handleException(e: Error, isFatal: boolean) {
  // Workaround for reporting errors caused by `throw 'some string'`
  // Unfortunately there is no way to figure out the stacktrace in this
  // case, so if you ended up here trying to trace an error, look for
  // `throw '<error message>'` somewhere in your codebase.
  if (!e.message) {
    e = new Error(e);
  }
  if (console._errorOriginal) {
    console._errorOriginal(e.message);
  } else {
    console.error(e.message);
  }
  reportException(e, isFatal);
}

function reactConsoleErrorHandler() {
  console._errorOriginal.apply(console, arguments);
  if (!console.reportErrorsAsExceptions) {
    return;
  }

  if (arguments[0] && arguments[0].stack) {
    reportException(arguments[0], /* isFatal */ false);
  } else {
    const stringifySafe = require('stringifySafe');
    const str = Array.prototype.map.call(arguments, stringifySafe).join(', ');
    if (str.slice(0, 10) === '"Warning: ') {
      // React warnings use console.error so that a stack trace is shown, but
      // we don't (currently) want these to show a redbox
      // (Note: Logic duplicated in polyfills/console.js.)
      return;
    }
    const error: ExtendedError = new Error('console.error: ' + str);
    error.framesToPop = 1;
    reportException(error, /* isFatal */ false);
  }
}

/**
 * Shows a redbox with stacktrace for all console.error messages.  Disable by
 * setting `console.reportErrorsAsExceptions = false;` in your app.
 */
function installConsoleErrorReporter() {
  // Enable reportErrorsAsExceptions
  if (console._errorOriginal) {
    return; // already installed
  }
  // Flow doesn't like it when you set arbitrary values on a global object
  console._errorOriginal = console.error.bind(console);
  console.error = reactConsoleErrorHandler;
  if (console.reportErrorsAsExceptions === undefined) {
    // Individual apps can disable this
    // Flow doesn't like it when you set arbitrary values on a global object
    console.reportErrorsAsExceptions = true;
  }
}

module.exports = {handleException, installConsoleErrorReporter};

We can learn from the method name with good semantics and clear annotation
It exposed two methods

  1. handleException——Throughconsole.error() & reportException()To deal with all casesthrow '<error message>'Method;
  2. installConsoleErrorReporter——Overloadconsole.errorAs long as it is usedconsole.errorThe printed information will display the error stack information in the form of “red screen”. Support settingsconsole.reportErrorsAsExceptions = false;Turn this behavior off.

At this stage of analysis, we can clearly feel that everything points toconsole.errormethod!!

We continue to query in the react native source code to find theinstallConsoleErrorReporter()Method in
node_modules\react-native\Libraries\Core\setUpErrorHandling.jsCalled in:

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */
'use strict';

/**
 * Sets up the console and exception handling (redbox) for React Native.
 * You can use this module directly, or just require InitializeCore.
 */
const ExceptionsManager = require('ExceptionsManager');
ExceptionsManager.installConsoleErrorReporter();

// Set up error handler
if (!global.__fbDisableExceptionsManager) {
  const handleError = (e, isFatal) => {
    try {
      ExceptionsManager.handleException(e, isFatal);
    } catch (ee) {
      console.log('Failed to print error: ', ee.message);
      throw e;
    }
  };

  const ErrorUtils = require('ErrorUtils');
  ErrorUtils.setGlobalHandler(handleError);
}

Its notes clearly point out that:“Set console and exception handling (red screen) for react native”

The core setting code is as follows:

const ErrorUtils = require('ErrorUtils');
  ErrorUtils.setGlobalHandler(handleError); //  That's where we're going

This is the ultimate entry point we’re looking for. All exceptions are created byErrorUtils.setGlobalHandlerAs long as we set it to our own defined callback function, we can take over the exception handling right from RN hands!!!
For example:

global.ErrorUtils.setGlobalHandler(e=> {
      /*Exception handling*/
      Console.log (%% C handle exception..... ','font- size:12px; color:#869')
      console.log(e.message)
      // do something to handle exception
      //...
    })

Nice ~, let’s continue to look for the reason for the yellow box.


2. Find the yellow screen entry point

Different from the red screen error reasons, students familiar with JS development should know that the only one that can output warning information is the callconsole.warn(). In the above yellow screen prompt, the stack tracking information is not printed, but we can open the debug mode (developer Menu > debug JS remotely) to see more detailed stack tracking information on the console
[source code analysis] may be the most practical solution for react native exception in the whole network [suggest collection]

Obviously, the yellow screen is prompted byYellowBox.jsOutput.
Continue to check the RN source code and find its location:node_modules\react-native\Libraries\YellowBox\YellowBox.jsThe contents are as follows:

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 */

'use strict';

const React = require('React');

import type {Category} from 'YellowBoxCategory';
import type {Registry, Subscription} from 'YellowBoxRegistry';

type Props = $ReadOnly<{||}>;
type State = {|
  registry: ?Registry,
|};

let YellowBox;

/**
 * YellowBox displays warnings at the bottom of the screen.
 *
 * Warnings help guard against subtle yet significant issues that can impact the
 * quality of the app. This "in your face" style of warning allows developers to
 * notice and correct these issues as quickly as possible.
 *
 * YellowBox is only enabled in `__DEV__`. Set the following flag to disable it:
 *
 *   console.disableYellowBox = true;
 *
 * Ignore specific warnings by calling:
 *
 *   YellowBox.ignoreWarnings(['Warning: ...']);
 *
 * Strings supplied to `YellowBox.ignoreWarnings` only need to be a substring of
 * the ignored warning messages.
 */
if (__DEV__) {
  const Platform = require('Platform');
  const RCTLog = require('RCTLog');
  const YellowBoxList = require('YellowBoxList');
  const YellowBoxRegistry = require('YellowBoxRegistry');

  const {error, warn} = console;

  // eslint-disable-next-line no-shadow
  YellowBox = class YellowBox extends React.Component<Props, State> {
    static ignoreWarnings(patterns: $ReadOnlyArray<string>): void {
      YellowBoxRegistry.addIgnorePatterns(patterns);
    }

    static install(): void {
      (console: any).error = function(...args) {
        error.call(console, ...args);
        // Show YellowBox for the `warning` module.
        if (typeof args[0] === 'string' && args[0].startsWith('Warning: ')) {
          registerWarning(...args);
        }
      };

      (console: any).warn = function(...args) {
        warn.call(console, ...args);
        registerWarning(...args);
      };

      if ((console: any).disableYellowBox === true) {
        YellowBoxRegistry.setDisabled(true);
      }
      (Object.defineProperty: any)(console, 'disableYellowBox', {
        configurable: true,
        get: () => YellowBoxRegistry.isDisabled(),
        set: value => YellowBoxRegistry.setDisabled(value),
      });

      if (Platform.isTesting) {
        (console: any).disableYellowBox = true;
      }

      RCTLog.setWarningHandler((...args) => {
        registerWarning(...args);
      });
    }

    static uninstall(): void {
      (console: any).error = error;
      (console: any).warn = error;
      delete (console: any).disableYellowBox;
    }

    _subscription: ?Subscription;

    state = {
      registry: null,
    };

    render(): React.Node {
      // TODO: Ignore warnings that fire when rendering `YellowBox` itself.
      return this.state.registry == null ? null : (
        <YellowBoxList
          onDismiss={this._handleDismiss}
          onDismissAll={this._handleDismissAll}
          registry={this.state.registry}
        />
      );
    }

    componentDidMount(): void {
      this._subscription = YellowBoxRegistry.observe(registry => {
        this.setState({registry});
      });
    }

    componentWillUnmount(): void {
      if (this._subscription != null) {
        this._subscription.unsubscribe();
      }
    }

    _handleDismiss = (category: Category): void => {
      YellowBoxRegistry.delete(category);
    };

    _handleDismissAll(): void {
      YellowBoxRegistry.clear();
    }
  };

  const registerWarning = (...args): void => {
    YellowBoxRegistry.add({args, framesToPop: 2});
  };
} else {
  YellowBox = class extends React.Component<Props> {
    static ignoreWarnings(patterns: $ReadOnlyArray<string>): void {
      // Do nothing.
    }

    static install(): void {
      // Do nothing.
    }

    static uninstall(): void {
      // Do nothing.
    }

    render(): React.Node {
      return null;
    }
  };
}

module.exports = YellowBox;

It is a class component. The logic is as follows:“Hijack the console. Warn of the host environment, and use the nativeYellowBoxListRendering out;At the same time, it also hijacks console.error to restore the warning information output at the error level in the react environment to the warning level log (to avoid affecting understanding, this need not be ignored) “

This is the starting point of the yellow screen. It just outputs the warning log in another way. It seems that it has nothing to do with what we are going to do, but is it really irrelevant?

Always remember that every error and warn level log of the application should not be ignored, especially the warn level log!

Let’s look at the following code:

//Simulating asynchronous operations may be requests or communicating with native modules methods
  mockAsyncHandle = ()=>{
    return new Promise((resolve,reject)=>{
      //Execution exception
      throw new Error([1,2,3].toString())
    })
  }

  async componentDidMount(){
    const resp = await this.mockAsyncHandle() //Execution exception
    //Subsequent code is no longer executed
    console.log(resp)
    //Using resp to do business processing may be a prerequisite for updating state or some operations
    // ...
  }

This code will trigger a yellow box prompt, and the warning level log is as follows:
[source code analysis] may be the most practical solution for react native exception in the whole network [suggest collection]

Students who have rich experience in using promise may have found that here,throw new Error([1,2,3].toString())The exception that was thrown was swallowed and the dependency in the code was ignoredrespThe logic of all willFailure, very serious exception! You might think of chained callsPromise.prototye.catch()To handle the promise in the rejected state, but ifcatchContinue to throw exceptions in the handler? This phenomenon is known in JavaScript you don’t know“The trap of despair”Like try… Catch, it always swallows the last exception.

On the web side, the browser will automatically track the memory usage, process the rejected project through the garbage collection mechanism, and provideunhandledrejectionEvent monitoring.

So, in RN, how to handle such promise exception?

View the source codenode_modules\react-native\Libraries\Promise.jsSo RN extends ES6 promise

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @flow
 */

'use strict';

const Promise = require('promise/setimmediate/es6-extensions');
require('promise/setimmediate/done');

Promise.prototype.finally = function(onSettled) {
  return this.then(onSettled, onSettled);
};

if (__DEV__) {
  /* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an
   * error found when Flow v0.54 was deployed. To see the error delete this
   * comment and run Flow. */
  require('promise/setimmediate/rejection-tracking').enable({
    allRejections: true,
    onUnhandled: (id, error = {}) => {
      let message: string;
      let stack: ?string;

      const stringValue = Object.prototype.toString.call(error);
      if (stringValue === '[object Error]') {
        message = Error.prototype.toString.call(error);
        stack = error.stack;
      } else {
        /* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses
         * an error found when Flow v0.54 was deployed. To see the error delete
         * this comment and run Flow. */
        message = require('pretty-format')(error);
      }

      const warning =
        `Possible Unhandled Promise Rejection (id: ${id}):\n` +
        `${message}\n` +
        (stack == null ? '' : stack);
      console.warn(warning);
    },
    onHandled: id => {
      const warning =
        `Promise Rejection Handled (id: ${id})\n` +
        'This means you can ignore any previous messages of the form ' +
        `"Possible Unhandled Promise Rejection (id: ${id}):"`;
      console.warn(warning);
    },
  });
}

module.exports = Promise;

RN is in the development environment by defaultpromise/setimmediate/rejection-trackingTo trace the rejected state of the project, and provide theonUnhandledThe callback function handles the unprocessed rejected project, and its execution time can berejection-tracking.jsIn the source code:

//...
timeout: setTimeout(
    onUnhandled.bind(null, promise._51),
    // For reference errors and type errors, this almost always
    // means the programmer made a mistake, so log them after just
    // 100ms
    // otherwise, wait 2 seconds to see if they get handled
    matchWhitelist(err, DEFAULT_WHITELIST)
      ? 100
      : 2000
  ),
//...

Similar to error handling, weJust putonUnhandledReplace the callback function with our custom promise exception handling logic, and you can take over the promise exception handling from RN hands!!!

OK, through the analysis of the source code, we have sorted out the ideas and know how to do it. Next, let’s start to realize it.

Perfect solution

Scheme: error bounds + error utils + project rejection tracking

It is mentioned in the foreword:

When there is an exception, we should comfort the user by demoting the UI (such as the common 404 page on the web side and the pop-up window of “network lost, please try again later”), and guide the user to turn to the normal page.

For example, the following prompt (Demo)
[source code analysis] may be the most practical solution for react native exception in the whole network [suggest collection]

Students with experience in react development should know that react 16 + provides a solution: error boundaries, which perfectly meets our logical requirements.
The official demo is as follows:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    //Update the state to display the degraded UI in the next rendering
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    //You can also report the error log to the server
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      //You can customize the degraded UI and render it
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

But the error boundary has the following defects

Error boundaryunableCapture errors generated in the following scenarios:

  • Event handling (learn more)
  • Asynchronous code (for examplesetTimeoutorrequestAnimationFrameCallback function)
  • Server side rendering (this can be ignored in RN)
  • Errors thrown by itself (not its subcomponents)

Fortunately, through our analysis of the above source code, we can pass through the error boundaryglobal.ErrorUtils.setGlobalHandler(callback)Register RN error handling callback function and settingsrejection-tracking.jsOfonUnhandledFunction to handle the unhandled rejected project

Let’s take a look at the final code after modification. Upgrade error boundary:

import React from 'react'
import PropTypes from 'prop-types'

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false }

    global.ErrorUtils.setGlobalHandler(e=> {
      /*Your exception handling logic*/
      Console.log (%% C handle exception..... ','font- size:12px; color:#869')
      console.log(e.message)
      this.setState({
        hasError: true
      })
    })
    require('promise/setimmediate/rejection-tracking').enable({
      allRejections: true,
      onUnhandled: (id, error = {}) => {
        let message
        let stack
  
        const stringValue = Object.prototype.toString.call(error);
        if (stringValue === '[object Error]') {
          message = Error.prototype.toString.call(error);
          stack = error.stack;
        } else {
          /* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses
           * an error found when Flow v0.54 was deployed. To see the error delete
           * this comment and run Flow. */
          message = require('pretty-format')(error);
        }
  
        const warning =
          `Possible Unhandled Promise Rejection (id: ${id}):\n` +
          `${message}\n` +
          (stack == null ? '' : stack);
        console.warn(warning);
        //Update the state to display the degraded UI in the next rendering
        this.setState({
          hasError: true
        })
      },
      onHandled: id => {
        const warning =
          `Promise Rejection Handled (id: ${id})\n` +
          'This means you can ignore any previous messages of the form ' +
          `"Possible Unhandled Promise Rejection (id: ${id}):"`;
        console.warn(warning);
      },
    });
  }

  static propTypes={
    //Customize UI after demotion
    errorPage:PropTypes.element,
    //You can add other attributes according to your actual business needs, such as whether to turn off red / yellow screen display in configuration development mode
  }

  static getDerivedStateFromError(error) {
    //Update the state to display the degraded UI in the next rendering
    return { hasError: true }
  }

  componentDidCatch(error, errorInfo) {
    //You can also report the error log to the server
    console.log(error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      //You can customize the degraded UI and render it
      return this.props.errorPage? this.props.errorPage:<h1>Something went wrong.</h1>
    }

    return this.props.children
  }
}
export default ErrorBoundary

The use method is the same as that of the error boundary. At the top of the component tree, that is, wrapping the root component, use:

//Errorpage is your custom demoted display UI
<ErrorBoundary errorPage={<ErrorPage/>}>
  <App/>
</ErrorBoundary>

Errorpage is your custom demoted display UI

Perfect. Since then, all the exceptions used in RN applications have been handled by us! Go to the project and try it

note appended

The source code analysis of react native in this article comes from version 0.59.9, but I also checked and analyzed the latest version 0.62.2. Except for some new files, the API involved in this article has not undergone any destructive changes. Please rest assured.

In addition, it is reported that the restructuring of react native architecture will be completed in the fourth quarter of 2020, that is, this year. The architecture evolution is as follows:
[source code analysis] may be the most practical solution for react native exception in the whole network [suggest collection]

Image from react native maintainer Lorenzo s

I hope react native can bring us better development and use experience!

FAQ

Finally, answer some questions you may have

  1. Why not try… Catch?
    A: it’s impossible to determine which code block will have exceptions. There will be performance problems when using try… Catch a lot, and it can only catch exceptions in synchronous code, but it can’t do anything about exceptions that may appear in asynchronous code; In addition, it also has the problem of “desperate trap”.
  2. Can error utils catch asynchronous exceptions?
    A: Yes. Any exception thrown by an RN application will be caught by errorautils.
  3. Why can’t errorutils catch exceptions in promise?
    A: for JSC, there is no error at this time, so it can’t be captured. What we call a project exception is that a rejected project has not been handled due to a design defect in the project, which is manifested as: the exception has been swallowed. So we need to defineonUnhandledTo deal with.
  4. Can function component be used to write error bounds?
    A: No. The error boundary can only be a class component. If you want to separate the error utils and promise exception handling from the error boundary, you can put them into other functional components. However, from the perspective of component-based design, this is not recommended.

statement

Original sharing is not easy, if you think it’s helpful, please like it.
The reprint should be approved by me, and a link to the original should be attached.
thank you!

Recommended Today

Analysis of super comprehensive MySQL statement locking (Part 1)

A series of articles: Analysis of super comprehensive MySQL statement locking (Part 1) Analysis of super comprehensive MySQL statement locking (Part 2) Analysis of super comprehensive MySQL statement locking (Part 2) Preparation in advance Build a system to store heroes of the Three KingdomsheroTable: CREATE TABLE hero ( number INT, name VARCHAR(100), country varchar(100), PRIMARY […]