Skip to content

Commit

Permalink
add default early JS error handler
Browse files Browse the repository at this point in the history
Summary:
Removing JavaScript error handler supplied to ReactHostImpl.java which is just a stub and creating a default handler in ReactInstance.java.

Changelog: [Internal]

- Provide default error handler that can handle early JavaScript error

**Motivation**
When app bundle is being loading and before JavaScript side error handling logic is executed, error that occur in between may be lost or not reported. Native code does have various `try/catch` statements to catch errors but in Android the error handler callback is implemented to be  passed in while instantiating `ReactHostImpl` where many cases, including OSS, are just stub implementation that does not do anything.

__The goal of this diff is to add a default error handler that can be used by both OSS and intern.__

When the JavaScript side error handling is fully setup any JavaScript error is routed to LogBox.
For early errors, we want to utilize the native RedBox to display errors.

Longer term goal is to have a single native pipeline but this diff is just to cover the early JS errors until this is realized.

**Implementation details**
The code changes were taken from previously abandoned diff by RSNara (D55439412).

- **Previous**
  - `ReactJsExceptionHandler` (often just a stub) is created in Java and passed into `ReactHostImpl`.

- **Updated**
  - `DefaultReactJsExceptionHandler` defined in `ReactInstance.java` is instantiated and passed into JNI/C++ via `initHybrid()` call.
  - `ReactJsExceptionHandler.reportJsException()` method is removed from `FbReactExceptionManager` as we are not using the default implementation above.

**Related questions**
ThreadQueue in Java uses a separate error handler. Question is if there a good reason for having a separate error handler of if we can combine this into a single error handler.

Also since ThreadQueues are implemented in Java, there is a question of supporting the error handling via native C++ would be feasible or desirable.

**Testing**
You use the following preview diff to `throw Error` before error handling code is run during JavaScript bundle setup. It also includes Android log to check the method was called.

`jf get --version 229685487`

Testing was done using `fb4a` and `rntester-android`.

**Note on inconsistent exception message in `rntester-android`**.
Currently the only the first exception is displayed in RedBox and rest are ignored. Depending on the timing, early js exception is reported or `SurfaceRegistryBinding::startSurface()` is shown. This seems to be more related to clean up when `ReactInstance.loadJSScript()` throws and is not covered in this diff.

Differential Revision: D58385767
  • Loading branch information
alanleedev authored and facebook-github-bot committed Jun 11, 2024
1 parent 8a15e0d commit 039fb64
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 21 deletions.
4 changes: 2 additions & 2 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -3773,8 +3773,8 @@ public abstract class com/facebook/react/runtime/JSRuntimeFactory {
}

public class com/facebook/react/runtime/ReactHostImpl : com/facebook/react/ReactHost {
public fun <init> (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;Lcom/facebook/react/interfaces/exceptionmanager/ReactJsExceptionHandler;ZZ)V
public fun <init> (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;ZLcom/facebook/react/interfaces/exceptionmanager/ReactJsExceptionHandler;Z)V
public fun <init> (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;ZZ)V
public fun <init> (Landroid/content/Context;Lcom/facebook/react/runtime/ReactHostDelegate;Lcom/facebook/react/fabric/ComponentFactory;ZZ)V
public fun addBeforeDestroyListener (Lkotlin/jvm/functions/Function0;)V
public fun addReactInstanceEventListener (Lcom/facebook/react/ReactInstanceEventListener;)V
public fun createSurface (Landroid/content/Context;Ljava/lang/String;Landroid/os/Bundle;)Lcom/facebook/react/interfaces/fabric/ReactSurface;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import com.facebook.react.bridge.ReactContext
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.common.build.ReactBuildConfig
import com.facebook.react.fabric.ComponentFactory
import com.facebook.react.interfaces.exceptionmanager.ReactJsExceptionHandler
import com.facebook.react.runtime.JSCInstance
import com.facebook.react.runtime.ReactHostImpl
import com.facebook.react.runtime.cxxreactpackage.CxxReactPackage
Expand Down Expand Up @@ -74,8 +73,6 @@ public object DefaultReactHost {
reactPackages = packageList,
jsRuntimeFactory = jsRuntimeFactory,
turboModuleManagerDelegateBuilder = defaultTmmDelegateBuilder)
// TODO: T180971255 Improve default exception handler
val reactJsExceptionHandler = ReactJsExceptionHandler { _ -> }
val componentFactory = ComponentFactory()
DefaultComponentsRegistry.register(componentFactory)
// TODO: T164788699 find alternative of accessing ReactHostImpl for initialising reactHost
Expand All @@ -85,7 +82,6 @@ public object DefaultReactHost {
defaultReactHostDelegate,
componentFactory,
true /* allowPackagerServerAccess */,
reactJsExceptionHandler,
useDevSupport,
)
.apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
import com.facebook.react.fabric.ComponentFactory;
import com.facebook.react.fabric.FabricUIManager;
import com.facebook.react.interfaces.TaskInterface;
import com.facebook.react.interfaces.exceptionmanager.ReactJsExceptionHandler;
import com.facebook.react.interfaces.fabric.ReactSurface;
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags;
import com.facebook.react.modules.appearance.AppearanceModule;
Expand Down Expand Up @@ -102,7 +101,6 @@ public class ReactHostImpl implements ReactHost {
private final Context mContext;
private final ReactHostDelegate mReactHostDelegate;
private final ComponentFactory mComponentFactory;
private final ReactJsExceptionHandler mReactJsExceptionHandler;
private final DevSupportManager mDevSupportManager;
private final Executor mBGExecutor;
private final Executor mUIExecutor;
Expand Down Expand Up @@ -143,15 +141,13 @@ public ReactHostImpl(
ReactHostDelegate delegate,
ComponentFactory componentFactory,
boolean allowPackagerServerAccess,
ReactJsExceptionHandler reactJsExceptionHandler,
boolean useDevSupport) {
this(
context,
delegate,
componentFactory,
Executors.newSingleThreadExecutor(),
Task.UI_THREAD_EXECUTOR,
reactJsExceptionHandler,
allowPackagerServerAccess,
useDevSupport);
}
Expand All @@ -162,15 +158,13 @@ public ReactHostImpl(
ComponentFactory componentFactory,
Executor bgExecutor,
Executor uiExecutor,
ReactJsExceptionHandler reactJsExceptionHandler,
boolean allowPackagerServerAccess,
boolean useDevSupport) {
mContext = context;
mReactHostDelegate = delegate;
mComponentFactory = componentFactory;
mBGExecutor = bgExecutor;
mUIExecutor = uiExecutor;
mReactJsExceptionHandler = reactJsExceptionHandler;
mQueueThreadExceptionHandler = ReactHostImpl.this::handleHostException;
mMemoryPressureRouter = new MemoryPressureRouter(context);
mAllowPackagerServerAccess = allowPackagerServerAccess;
Expand Down Expand Up @@ -1067,7 +1061,6 @@ private Task<ReactInstance> getOrCreateReactInstanceTask() {
mComponentFactory,
devSupportManager,
mQueueThreadExceptionHandler,
mReactJsExceptionHandler,
mUseDevSupport,
getOrCreateReactHostInspectorTarget());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@

package com.facebook.react.runtime;

import static com.facebook.react.util.JSStackTrace.COLUMN_KEY;
import static com.facebook.react.util.JSStackTrace.FILE_KEY;
import static com.facebook.react.util.JSStackTrace.LINE_NUMBER_KEY;
import static com.facebook.react.util.JSStackTrace.METHOD_NAME_KEY;

import android.content.res.AssetManager;
import android.view.View;
import com.facebook.common.logging.FLog;
import com.facebook.fbreact.specs.NativeExceptionsManagerSpec;
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.infer.annotation.ThreadConfined;
import com.facebook.infer.annotation.ThreadSafe;
Expand All @@ -21,12 +28,15 @@
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.JSBundleLoader;
import com.facebook.react.bridge.JSBundleLoaderDelegate;
import com.facebook.react.bridge.JavaOnlyArray;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.JavaScriptContextHolder;
import com.facebook.react.bridge.NativeArray;
import com.facebook.react.bridge.NativeMap;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactNoCrashSoftException;
import com.facebook.react.bridge.ReactSoftExceptionLogger;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.RuntimeExecutor;
import com.facebook.react.bridge.RuntimeScheduler;
import com.facebook.react.bridge.queue.MessageQueueThread;
Expand Down Expand Up @@ -110,7 +120,6 @@ final class ReactInstance {
ComponentFactory componentFactory,
DevSupportManager devSupportManager,
QueueThreadExceptionHandler exceptionHandler,
ReactJsExceptionHandler reactExceptionManager,
boolean useDevSupport,
@Nullable ReactHostInspectorTarget reactHostInspectorTarget) {
mBridgelessReactContext = bridgelessReactContext;
Expand Down Expand Up @@ -154,14 +163,15 @@ final class ReactInstance {
// Notify JS if profiling is enabled
boolean isProfiling =
Systrace.isTracing(Systrace.TRACE_TAG_REACT_APPS | Systrace.TRACE_TAG_REACT_JS_VM_CALLS);

mHybridData =
initHybrid(
jsRuntimeFactory,
jsMessageQueueThread,
nativeModulesMessageQueueThread,
mJavaTimerManager,
jsTimerExecutor,
reactExceptionManager,
new DefaultReactJsExceptionHandler(nativeModulesMessageQueueThread),
bindingsInstaller,
isProfiling,
reactHostInspectorTarget);
Expand Down Expand Up @@ -311,6 +321,44 @@ public ReactQueueConfiguration getReactQueueConfiguration() {
return mQueueConfiguration;
}

class DefaultReactJsExceptionHandler implements ReactJsExceptionHandler {
private MessageQueueThread nativeModulesMessageQueueThread;

DefaultReactJsExceptionHandler(MessageQueueThread nativeModulesMessageQueueThread) {
this.nativeModulesMessageQueueThread = nativeModulesMessageQueueThread;
}

@Override
public void reportJsException(ParsedError error) {
List<ReactJsExceptionHandler.ParsedError.StackFrame> frames = error.getFrames();
List<ReadableMap> readableMapList = new ArrayList<>();
for (ReactJsExceptionHandler.ParsedError.StackFrame frame : frames) {
JavaOnlyMap map = new JavaOnlyMap();
map.putDouble(COLUMN_KEY, frame.getColumnNumber());
map.putDouble(LINE_NUMBER_KEY, frame.getLineNumber());
map.putString(FILE_KEY, (String) frame.getFileName());
map.putString(METHOD_NAME_KEY, (String) frame.getMethodName());
readableMapList.add(map);
}

JavaOnlyMap data = new JavaOnlyMap();
data.putString("message", error.getMessage());
data.putArray("stack", JavaOnlyArray.from(readableMapList));
data.putInt("id", error.getExceptionId());
data.putBoolean("isFatal", error.isFatal());

// Simulate async native module method call
nativeModulesMessageQueueThread.runOnQueue(
() -> {
NativeExceptionsManagerSpec exceptionsManager =
(NativeExceptionsManagerSpec)
Assertions.assertNotNull(
mTurboModuleManager.getModule(NativeExceptionsManagerSpec.NAME));
exceptionsManager.reportException(data);
});
}
}

public void loadJSBundle(JSBundleLoader bundleLoader) {
// Load the JS bundle
Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ReactInstance.loadJSBundle");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,7 @@ class ReactHostTest {
Mockito.doReturn(jSBundleLoader).`when`(reactHostDelegate).jsBundleLoader
reactHost =
ReactHostImpl(
activityController.get().application,
reactHostDelegate,
componentFactory,
false,
{},
false)
activityController.get().application, reactHostDelegate, componentFactory, false, false)
val taskCompletionSource = TaskCompletionSource<Boolean>().apply { setResult(true) }
mockedTaskCompletionSourceCtor =
Mockito.mockConstruction(
Expand Down

0 comments on commit 039fb64

Please sign in to comment.