preface

When doing React Native mixed development, the production environment sometimes encounters the situation of opening the RN (i.e., React Native for short) application white screen, operating in the RN page to the Native page or directly causing the APP Crash. Through the analysis of APP logs, the reasons can be classified into the following two categories:

  1. JS layer compiler run times an error. It is generally due to some special data or situation resulting in JS implementation error report;
  2. An exception occurs when JS interprets native UI or communicates with native modules.

For the first point, you can quickly log down the offending JS code and solve it, but for the second point, it is often the underlying code execution error 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: value@28: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: Ci@89:62783
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: qi@89:66674
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: ea@89: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: unstable_runWithPriority@164:3238
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: ja@89:81253
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Oa@89:81007
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Wa@89:80310
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Aa@89:79323
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Ki@89:68624
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Ki@-1
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: yt@89:21420
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: y@115:657
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: callTimers@115:2816
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@28: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: value@28:2565
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@28:794
06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@-1
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 worst thing you can do is have an exception. The worst thing you can do is have an exception, which is a bad experience for the user, even though it happens very rarely. We should remind and comfort the user and guide the user to the normal page by downgrading the UI (such as the common 404 page on the Web side, the “network is down, please try again later” pop-up window) in case of abnormal occurrence. Unfortunately, we don’t usually have that right now. The React Native framework does all the exception handling itself. Therefore, we need to take over exception handling from React Native to implement our own logic (similar to the idea of inversion of control).

Next, I will lead you to analyze and implement step by step.

Analyze React Native’s red/yellow screen prompts

Regardless of the cause of the RN application exception, in the development mode environment (which is automatically disabled in Release/Production), by default, a full screen message will appear in red box or yellow box:

Note that in this text, both error and warning are treated as exceptions

Red screen:



Huang ping:

In the official description:

In-app errors are displayed in full-screen red (debug mode). We call them red box errors. You can manually trigger red screen errors by using 'console.error()'. Alerts in the app will appear in full screen yellow in the app (debug mode). We call this the yellow box error. Click the alert to see the details or ignore it. Similar to the red screen alarm, you can use 'console.warn()' to manually trigger the yellow screen warning.

These two full-screen prompts are React Native’s handling of RN exceptions.

So here’s the idea. All we need to do is find the place where RN pops up in the red and yellow screens, and replace it with our own business logic.

The schematic diagram is as follows:

OK, next we need to find this entry from the source code, do not be afraid of the source code, follow my train of thought, let’s go!

Find the entry point from the source code

1. Find the pointcut for the red screen

In the red screen image above, we triggered the red screen prompt via console.error(‘I am red box’). The error stack trace 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]

Where, indicates the file location where the error occurred:

\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

Querying console.error in each file in turn, you can find the following comment in the showErrorDialog method in the reactNativerEnderer-dev.js file:

  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 the native red screen controller.

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

Interception of lifecycle errors and ensure that the correct stack trace is displayed in Native Redbox components Perfect, we found the reason for 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.

“The reason the call to console.error will automatically navigate to the native red screen controller is because we already called ExceptionsManager on it.”

So at this point, we can think of a red screen === = because of what ExceptionsManager did and what we need to do is replace the logic of the ExceptionsManager implementation with our own logic!

Tip: look carefully in the source code
showErrorDialog()Where it was called, and you’ll find it
logCapturedError()And above that
logError()Analysis,
logError(), you’ll notice that the original React
Error boundaryIt also has something to do with being able to catch errors when rendering components

ExceptionsManager.js: Node_Modules \ React-Native \Libraries\Core\ExceptionsManager.js: Node_Modules \ React-Native \Libraries\Core\ExceptionsManager.js:

/** * 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};

As we can see from the semantically good method names and clear comments, it exposes two methods:

  1. handleException– byconsole.error() & reportException()Deal with all kinds ofthrow '<error message>'The exception thrown by the
  2. installConsoleErrorReporter– overloadedconsole.error, as long as it is usedconsole.errorPrinted messages will show the error stack in a red screen. Allows you to setconsole.reportErrorsAsExceptions = false;Turn this behavior off.

At this point in the analysis, it’s clear that everything points toconsole.errorMethod!!!!!

We continue to query in the React Native source code, Find installConsoleErrorReporter () method in node_modules/react – native/Libraries/Core \ setUpErrorHandling js is called:

/**
 * 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);
}

The note says very clearly, “Set Console and Exception Handling for React Native (red screen).”

The core setup code is:

const ErrorUtils = require('ErrorUtils'); ErrorUtils.setGlobalHandler(handleError); // This is where we come in

This is the final point, we are looking for all the abnormal by ErrorUtils. SetGlobalHandler callback function processing, as long as it is set as our own definition of the callback function can took exception from RN in discretion!!!!!! Such as:

Global. ErrorUtils. SetGlobalHandler (e = > {exception handling / * * / console in the log (' exception handling % c... ', 'font-size:12px; color:#869') console.log(e.message) // do something to handle exception //... })

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


2. Find out the entry point of yellow screen

Unlike the red screen error, those familiar with JS development should know that the only thing that can output a warning message is a callconsole.warn(). In the above yellow screen prompt, there is no stack tracking information printed, but we can open the Debug mode (developer menu -> Debug JS Remotally), you can see more detailed stack tracking information in the console:

Obviously, the yellow screen prompt is output by yellowbox.js. Node_Modules \ React-Native \Libraries\ Yellowbox \ Yellowbox.js Go ahead and look at the RN source code and find it: node_modules\ React-Native \Libraries\ Yellowbox \ Yellowbox.js

/** * 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 with a logic that reads, “Hijack the console.warn of the host environment and render the warning message with the native YellowBoxList; Also hijack console.error to restore the Error-level warnings from the React environment to a Warn-level log (don’t worry about that, so you don’t have to understand it).”

This is where the yellow screen comes in, just printing out the warning log in a different way that seems unrelated to what we’re trying to do, but is it?

Always remember that every error and WARN level log applied should not be ignored, especially WARN level logs!

Let’s look at the following code:

MockAsynchandle = ()=>{return new Promise((resolve,reject)=>{return new Promise((resolve,reject))=>{return new Promise((resolve,reject))=>{return new Promise((resolve,reject))=>{// Error([1,2,3].toString())})} async componentDidMount(){const resp = await this.mockAsynchandle () // Subsequent code will no longer execute console.log(resp) // use resp for business processing, either to update state or as a prerequisite for some operation //... }

This code will trigger a yellow box prompt with a warning level log as follows:

As those of you who have had a lot of experience with Promise may have noticed, the exception thrown by throw new Error([1,2,3].toString()) is swallowed, and all the resp dependent logic in your code will fail. A serious exception! You might think of a chain call to Promise.prototye.catch() to handle a Promise that rejects the state, but what if the catch handler continues to throw an exception? This phenomenon is referred to in the book “JavaScript You Don’t Know” as a “trap of despair”, in conjunction with try… Like a catch, it will always swallow the last exception.

On the Web side, the browser automatically tracks memory usage, processes the Rejected Promise through a garbage collection mechanism, and provides an UnhandledRejection event to listen on.

So, how do you handle such Promise exceptions in RN?

Node_modules \react-native\Libraries\ promise.js: 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 supfixme * 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 supfixme * 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 the default in the development environment, through the promise/setimmediate/rejection – tracking to track the promise of rejected state, The OnUnHandled callback handles the Rejected Promise, which can be found in the Reject-tracking.js source:

/ /... 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 the error handling, we simply replace the onUnhandled callback with our custom Promise exception handling logic to take over the Promise exception handling from RN!!

OK, now that we have a clear idea of what to do by analyzing the source code, let’s start implementing it.

Perfect solution

Solution: Error Boundary + ErrorUtils + Promise Rejection Tracking

It is mentioned in the preface:

We should console the user and direct the user to the normal page by demoting the UI (such as the common 404 page on the Web side and the pop-up window “the network is down, please try again later”) when an exception occurs.

For example, the following prompts (demo) :

For those of you who are React developers, React 16+ offers a protocol: Error Boundaries that fits perfectly with our logic. The official demo is as follows:

class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) {// Update state so that the next render will show the UI return {hasError: true}; } componentDidCatch(error, errorInfo) {// You can also report the error log to logErrorToMyService(error, errorInfo); } render() {if (this.state.hasError) {return <h1>Something went wrong.</h1>; } return this.props.children; }}

However, the error boundary has the following drawbacks:

The error boundary does not catch errors that occur in the following scenarios:

  • Event handling (learn more)
  • Asynchronous code (for examplesetTimeoutrequestAnimationFrameCallback function)
  • Server-side rendering (you can ignore this entry in RN)
  • Error thrown by itself (not its child components)

Fortunately, through our analysis of the above source code, we can pass within the error boundaryglobal.ErrorUtils.setGlobalHandler(callback)Register RN error handling callbacks and Settingsrejection-tracking.jstheonUnhandledFunction to handle unprocessed Rejected Promises.

Take a look at the revised final code, the updated 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 = > {/ * * / console your exception handling logic. The log (' exception handling % c... ', '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 supfixme * 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); // Updated state so that the next render can show the degraded UI 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); }}); } the static propTypes = {/ / custom UI errorPage: after their demotion propTypes. Element, / / can be according to their actual business needs to add other attributes, } static getDerivedStateFromError(error) {// Update state to show the next render of the degraded UI return {hasError:}} static getDerivedStateFromError(error) {// } componentDidCatch(error, errorInfo) {// You can also report the error log to server console.log(error, errorInfo) {// ErrorInfo)} render() {if (this.state.hasError) {// You can customize the degraded UI and render Return this.props.errorPage? this.props.errorPage:<h1>Something went wrong.</h1> } return this.props.children } } export default ErrorBoundary

It is used in the same way as the error boundary, at the top level of the component tree, that is, wrapping the root component:

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

ErrorPage is your custom degraded display UI

Perfect. From now on, all exceptions used in the RN application are handled by ourselves! Go to the project and try it

note

The React Native source code analysis in this article is all from version 0.59.9, but I also consulted and analyzed the latest source code of version 0.62.2. Except for the addition of some files, the API involved in this article has not been changed in a destructive way. Please feel free to eat it.

In addition, it has been announced that the React Native architecture refactoring will be completed in Q4 2020, which will be completed this year, with the following architectural changes:

Photo by React Native Maintainer Lorenzo S.

Hope React Native will bring us a better development and use experience!

FAQ

Finally, to answer a few questions you might have:

  1. Why don’t you try… catch? A: It is not possible to determine which code block is causing an exception, and heavy use of try… Catches have performance problems, and they can only catch exceptions in synchronous code, not in asynchronous code. There is also the “despair trap” problem.
  2. Can ErrorUtils catch asynchronous exceptions? A: Yes. Any exception thrown in the RN application will be caught by the ErrorUtils.
  3. Why can’t ErrorUtils catch exceptions in Promise?

    A: Because for JSC, no error has occurred at this time, and of course it cannot be caught. A Promise exception is a Promise design defect that causes a Rejected Promise to never be processed. So we need to defineonUnhandledTo process.
  4. Can I write an error boundary using Function Component? A: No. The error boundary can only be a Class component. If you want to take the ErrorUtils and Promise exception handling out of the error boundary and put it into other functional components, that’s fine, but from a modular design perspective, it’s not recommended.

The statement

Original sharing is not easy, feel helpful to you, welcome thumb up favorites. Reprint with my consent, and attach the link to the original. Thank you very much!