diff --git a/packages/react-native/Libraries/ReactNative/UIManager.js b/packages/react-native/Libraries/ReactNative/UIManager.js index e830b96971891a..ebeb10c9620df0 100644 --- a/packages/react-native/Libraries/ReactNative/UIManager.js +++ b/packages/react-native/Libraries/ReactNative/UIManager.js @@ -180,6 +180,22 @@ const UIManager = { commandName: number | string, commandArgs: any[], ) { + // Sometimes, libraries directly pass in the output of `findNodeHandle` to + // this function without checking if it's null. This guards against that + // case. We throw early here in Javascript so we can get a JS stacktrace + // instead of a harder-to-debug native Java or Objective-C stacktrace. + if (typeof reactTag !== 'number') { + let stringifiedArgs = '(failed to stringify commandArgs)'; + try { + stringifiedArgs = JSON.stringify(commandArgs); + } catch (err) { + // Do nothing. We have a default message + } + throw new Error( + `dispatchViewManagerCommand: found null reactTag with args ${stringifiedArgs}`, + ); + } + if (isFabricReactTag(reactTag)) { const FabricUIManager = nullthrows(getFabricUIManager()); const shadowNode = diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaMethodWrapper.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaMethodWrapper.java index 8a2ef7bb9fb6d2..810c59bc7423db 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaMethodWrapper.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaMethodWrapper.java @@ -356,7 +356,7 @@ public void invoke(JSInstance jsInstance, ReadableArray parameters) { mArgumentExtractors[i].extractArgument(jsInstance, parameters, jsArgumentsConsumed); jsArgumentsConsumed += mArgumentExtractors[i].getJSArgumentsNeeded(); } - } catch (UnexpectedNativeTypeException e) { + } catch (UnexpectedNativeTypeException | NullPointerException e) { throw new NativeArgumentsParseException( e.getMessage() + " (constructing arguments for " @@ -364,29 +364,36 @@ public void invoke(JSInstance jsInstance, ReadableArray parameters) { + " at argument index " + getAffectedRange( jsArgumentsConsumed, mArgumentExtractors[i].getJSArgumentsNeeded()) - + ")", + + ") with parameters " + + parameters.toArrayList(), e); } try { mMethod.invoke(mModuleWrapper.getModule(), mArguments); - } catch (IllegalArgumentException ie) { - throw new RuntimeException("Could not invoke " + traceName, ie); - } catch (IllegalAccessException iae) { - throw new RuntimeException("Could not invoke " + traceName, iae); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new RuntimeException(createInvokeExceptionMessage(traceName, parameters), e); } catch (InvocationTargetException ite) { // Exceptions thrown from native module calls end up wrapped in InvocationTargetException // which just make traces harder to read and bump out useful information if (ite.getCause() instanceof RuntimeException) { throw (RuntimeException) ite.getCause(); } - throw new RuntimeException("Could not invoke " + traceName, ite); + throw new RuntimeException(createInvokeExceptionMessage(traceName, parameters), ite); } } finally { SystraceMessage.endSection(TRACE_TAG_REACT_JAVA_BRIDGE).flush(); } } + /** + * Makes it easier to determine the cause of an error invoking a native method from Javascript + * code by adding the function and parameters. + */ + private static String createInvokeExceptionMessage(String traceName, ReadableArray parameters) { + return "Could not invoke " + traceName + " with parameters " + parameters.toArrayList(); + } + /** * Determines how the method is exported in JavaScript: METHOD_TYPE_ASYNC for regular methods * METHOD_TYPE_PROMISE for methods that return a promise object to the caller. METHOD_TYPE_SYNC