react-native-ble-plx icon indicating copy to clipboard operation
react-native-ble-plx copied to clipboard

🐛 Android: App crashes on device disconnect when using monitorCharacteristicForDevice()

Open LohenHM opened this issue 4 months ago • 12 comments

Prerequisites

  • [x] I checked the documentation and FAQ without finding a solution
  • [x] I checked to make sure that this issue has not already been filed

Bug Description

When using the monitorCharacteristicForDevice() method on Android, if the peripheral device disconnects unexpectedly, the application crashes instead of propagating the disconnection error to the monitoring listener callback. The expected behavior is for the listener (error: BleError | null, characteristic: Characteristic | null) => void to be invoked with a BleError object.

The crash is caused by a java.lang.NullPointerException in the native Android code, as the Promise.reject method is called with a null error code.

Expected Behavior

The listener for monitorCharacteristicForDevice() should be called with an error object detailing the disconnection, and the app should continue running without crashing.

Current Behavior

The application crashes with a FATAL EXCEPTION. The logs indicate a java.lang.NullPointerException because Promise.reject was called with a null code parameter.

Library version

3.5.0

Device

One Plus 12, Android 15

Environment info

- React Native: v0.81
- react-native-ble-plx: v3.5.0
- Platform: Android 15

Steps to reproduce

  1. Connect to a BLE peripheral device.
  2. Start monitoring a characteristic using device.monitorCharacteristicForDevice(...).
  3. Force the device to disconnect (e.g., by turning the peripheral off or moving it out of range).
  4. Observe the application crash.

Proposed Solution / Workaround

The issue appears to be in the onError callback within the monitorCharacteristicForDevice implementation in BlePlxModule.java. The safePromise is rejected with null as the first argument.

A temporary fix is to modify the native file to provide a valid error code string.

File: /node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java

line number: 791

bleAdapter.monitorCharacteristicForDevice(
      deviceId, serviceUUID, characteristicUUID, transactionId, subscriptionType,
      new OnEventCallback<Characteristic>() {
        @Override
        public void onEvent(Characteristic data) {
          WritableArray jsResult = Arguments.createArray();
          jsResult.pushNull();
          jsResult.pushMap(characteristicConverter.toJSObject(data));
          jsResult.pushString(transactionId);
          sendEvent(Event.ReadEvent, jsResult);
        }
      }, new OnErrorCallback() {
        @Override
        public void onError(BleError error) {
-         safePromise.reject(null, errorConverter.toJs(error));
+         safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
        }
      }
    );

Relevant log output

2025-09-04 15:53:51.086 17235-733   AndroidRuntime          com.example.dev                   E  FATAL EXCEPTION: RxComputationThreadPool-3 (Ask Gemini)
                                                                                                    Process: com.example.dev, PID: 17235
                                                                                                    io.reactivex.exceptions.CompositeException: 2 exceptions occurred. 
                                                                                                    	at io.reactivex.internal.subscribers.LambdaSubscriber.onError(LambdaSubscriber.java:82)
                                                                                                    	at io.reactivex.internal.operators.flowable.FlowableDoOnEach$DoOnEachSubscriber.onError(FlowableDoOnEach.java:111)
                                                                                                    	at io.reactivex.internal.operators.flowable.FlowableDoOnLifecycle$SubscriptionLambdaSubscriber.onError(FlowableDoOnLifecycle.java:85)
                                                                                                    	at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.checkTerminated(FlowableObserveOn.java:209)
                                                                                                    	at io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber.runAsync(FlowableObserveOn.java:399)
                                                                                                    	at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.run(FlowableObserveOn.java:176)
                                                                                                    	at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
                                                                                                    	at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
                                                                                                    	at java.util.concurrent.FutureTask.run(FutureTask.java:317)
                                                                                                    	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:348)
                                                                                                    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156)
                                                                                                    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651)
                                                                                                    	at java.lang.Thread.run(Thread.java:1119)
                                                                                                      ComposedException 1 :
                                                                                                    	com.polidea.rxandroidble2.exceptions.BleDisconnectedException: Disconnected from MAC='XX:XX:XX:XX:XX:XX' with status 8 (GATT_INSUF_AUTHORIZATION or GATT_CONN_TIMEOUT)
                                                                                                    		at com.polidea.rxandroidble2.internal.connection.RxBleGattCallback$2.onConnectionStateChange(RxBleGattCallback.java:81)
                                                                                                    		at android.bluetooth.BluetoothGatt$1$4.run(BluetoothGatt.java:390)
                                                                                                    		at android.bluetooth.BluetoothGatt.runOrQueueCallback(BluetoothGatt.java:1081)
                                                                                                    		at android.bluetooth.BluetoothGatt.-$$Nest$mrunOrQueueCallback(Unknown Source:0)
                                                                                                    		at android.bluetooth.BluetoothGatt$1.onClientConnectionState(BluetoothGatt.java:384)
                                                                                                    		at android.bluetooth.IBluetoothGattCallback$Stub.onTransact(IBluetoothGattCallback.java:209)
                                                                                                    		at android.os.Binder.execTransactInternal(Binder.java:1523)
                                                                                                    		at android.os.Binder.execTransact(Binder.java:1457)
                                                                                                    	Caused by: java.lang.NullPointerException: Parameter specified as non-null is null: method com.facebook.react.bridge.PromiseImpl.reject, parameter code
                                                                                                    		at com.facebook.react.bridge.PromiseImpl.reject(Unknown Source:2)
                                                                                                    		at com.bleplx.utils.SafePromise.reject(SafePromise.java:25)
                                                                                                    		at com.bleplx.BlePlxModule$41.onError(BlePlxModule.java:791)
                                                                                                    		at com.bleplx.adapter.utils.SafeExecutor.error(SafeExecutor.java:30)
                                                                                                    		at com.bleplx.adapter.BleModule.lambda$safeMonitorCharacteristicForDevice$45(BleModule.java:1485)
                                                                                                    		at com.bleplx.adapter.BleModule.$r8$lambda$JVuqIGnSfaxzZFLoyHm91xQUhdI(Unknown Source:0)
                                                                                                    		at com.bleplx.adapter.BleModule$$ExternalSyntheticLambda3.accept(D8$$SyntheticClass:0)
                                                                                                    		at io.reactivex.internal.subscribers.LambdaSubscriber.onError(LambdaSubscriber.java:79)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableDoOnEach$DoOnEachSubscriber.onError(FlowableDoOnEach.java:111)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableDoOnLifecycle$SubscriptionLambdaSubscriber.onError(FlowableDoOnLifecycle.java:85)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.checkTerminated(FlowableObserveOn.java:209)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber.runAsync(FlowableObserveOn.java:399)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.run(FlowableObserveOn.java:176)
                                                                                                    		at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
                                                                                                    		at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
                                                                                                    		at java.util.concurrent.FutureTask.run(FutureTask.java:317)
2025-09-04 15:53:51.086 17235-733   AndroidRuntime          com.example.dev                   E  		at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:348) (Ask Gemini)
                                                                                                    		at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156)
                                                                                                    		at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651)
                                                                                                    		at java.lang.Thread.run(Thread.java:1119)
                                                                                                      ComposedException 2 :
                                                                                                    	java.lang.NullPointerException: Parameter specified as non-null is null: method com.facebook.react.bridge.PromiseImpl.reject, parameter code
                                                                                                    		at com.facebook.react.bridge.PromiseImpl.reject(Unknown Source:2)
                                                                                                    		at com.bleplx.utils.SafePromise.reject(SafePromise.java:25)
                                                                                                    		at com.bleplx.BlePlxModule$41.onError(BlePlxModule.java:791)
                                                                                                    		at com.bleplx.adapter.utils.SafeExecutor.error(SafeExecutor.java:30)
                                                                                                    		at com.bleplx.adapter.BleModule.lambda$safeMonitorCharacteristicForDevice$45(BleModule.java:1485)
                                                                                                    		at com.bleplx.adapter.BleModule.$r8$lambda$JVuqIGnSfaxzZFLoyHm91xQUhdI(Unknown Source:0)
                                                                                                    		at com.bleplx.adapter.BleModule$$ExternalSyntheticLambda3.accept(D8$$SyntheticClass:0)
                                                                                                    		at io.reactivex.internal.subscribers.LambdaSubscriber.onError(LambdaSubscriber.java:79)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableDoOnEach$DoOnEachSubscriber.onError(FlowableDoOnEach.java:111)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableDoOnLifecycle$SubscriptionLambdaSubscriber.onError(FlowableDoOnLifecycle.java:85)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.checkTerminated(FlowableObserveOn.java:209)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber.runAsync(FlowableObserveOn.java:399)
                                                                                                    		at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.run(FlowableObserveOn.java:176)
                                                                                                    		at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
                                                                                                    		at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
                                                                                                    		at java.util.concurrent.FutureTask.run(FutureTask.java:317)
                                                                                                    		at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:348)
                                                                                                    		at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156)
                                                                                                    		at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651)
                                                                                                    		at java.lang.Thread.run(Thread.java:1119)
2025-09-04 15:53:51.088  3181-5341  OplusAppStartupMonitor  system_server                        D  notifyUnstableAppInfo: Bundle[{unstableTime=1756981431088, reason=crash, userId=0, exceptionMsg=Parameter specified as non-null is null: method com.facebook.react.bridge.PromiseImpl.reject, parameter code, exceptionClass=java.lang.NullPointerException, app_channel_type=unstable, packageName=com.example.dev, unstable_restrict_switch=true}]
2025-09-04 15:53:51.090  3181-5676  OplusEapMa...sendEvent: system_server                        W  com.example.dev happenedcrash
2025-09-04 15:53:51.101  3181-5676  ActivityTaskManager     system_server                        W    Force finishing activity com.example.dev/com.nilesecure.MainActivity

LohenHM avatar Sep 04 '25 14:09 LohenHM

Patch:

react-native-ble-plx+3.5.0.patch

diff --git a/node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java b/node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java
index c731640..22b438d 100644
--- a/node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java
+++ b/node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java
@@ -788,7 +788,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       }
     );

josmithua avatar Sep 16 '25 20:09 josmithua

This is not the only line to fix. I'm for example using monitorCharacteristic() instead of monitorCharacteristicForDevice(), and it has the same issue. And actually, all the other functions in this file have the same issue. Once I had a writeCharacteristic() error (due to some malformatted data) and it also made the app crash, with the same error.

karlvonladach avatar Sep 17 '25 07:09 karlvonladach

I'm having same issue after upgrading my project to expo 54. I'm using "monitorCharacteristicForService" function.

ERROR Your app just crashed. See the error below. java.lang.NullPointerException: Parameter specified as non-null is null: method com.facebook.react.bridge.PromiseImpl.reject, parameter code com.facebook.react.bridge.PromiseImpl.reject(Unknown Source:2) com.bleplx.utils.SafePromise.reject(SafePromise.java:25) com.bleplx.BlePlxModule$41.onError(BlePlxModule.java:791) com.bleplx.adapter.utils.SafeExecutor.error(SafeExecutor.java:30) com.bleplx.adapter.BleModule.lambda$safeMonitorCharacteristicForDevice$42(BleModule.java:1476) com.bleplx.adapter.BleModule.$r8$lambda$oguGTpV4M2u9BE2h87wB5iwi2-M(Unknown Source:0) com.bleplx.adapter.BleModule$$ExternalSyntheticLambda47.run(D8$$SyntheticClass:0) io.reactivex.internal.operators.flowable.FlowableDoOnLifecycle$SubscriptionLambdaSubscriber.cancel(FlowableDoOnLifecycle.java:115) io.reactivex.internal.subscribers.BasicFuseableSubscriber.cancel(BasicFuseableSubscriber.java:158) io.reactivex.internal.subscriptions.SubscriptionHelper.cancel(SubscriptionHelper.java:181) io.reactivex.internal.subscribers.LambdaSubscriber.cancel(LambdaSubscriber.java:119) io.reactivex.internal.subscribers.LambdaSubscriber.dispose(LambdaSubscriber.java:104) com.bleplx.adapter.utils.DisposableMap.removeSubscription(DisposableMap.java:24) com.bleplx.adapter.BleModule.cancelTransaction(BleModule.java:1044) com.bleplx.BlePlxModule.cancelTransaction(BlePlxModule.java:134) com.facebook.jni.NativeRunnable.run(Native Method) android.os.Handler.handleCallback(Handler.java:942) android.os.Handler.dispatchMessage(Handler.java:99) com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.kt:21) android.os.Looper.loopOnce(Looper.java:201) android.os.Looper.loop(Looper.java:288) com.facebook.react.bridge.queue.MessageQueueThreadImpl$Companion.startNewBackgroundThread$lambda$1(MessageQueueThreadImpl.kt:175) com.facebook.react.bridge.queue.MessageQueueThreadImpl$Companion.$r8$lambda$ldnZnqelhYFctGaUKkOKYj5rxo4(Unknown Source:0) com.facebook.react.bridge.queue.MessageQueueThreadImpl$Companion$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0) java.lang.Thread.run(Thread.java:1119)

fatihkayan20 avatar Sep 17 '25 13:09 fatihkayan20

I'm very thankful for this package and all the maintainers over the years, but I think it might be time to create a fork if it is going to remain unmaintained. At least I'd be interested in becoming a contributor to help get bug fixes in and released. Bluetooth connectivity is at the core of our work projects and we need a reliable solution that supports the new architecture.

josmithua avatar Sep 17 '25 14:09 josmithua

react-native-ble-plx+3.5.0.patch

For anyone having this issue, here's a working patch^

And here's a writeup (courtesy of mr Claude) for how to use it. @aliberski @angelos3lex this fix is tested working if you plan on updating:

`# Fix for react-native-ble-plx Android Crash on Disconnect

Problem

React Native apps using react-native-ble-plx crash on Android when BLE devices disconnect unexpectedly. This is caused by Promise.reject(null) calls in the native Android code, which crashes modern React Native versions that require non-null error codes.

Affected versions:

  • react-native-ble-plx: 3.2.1, 3.5.0
  • React Native: 0.81.4+
  • Expo SDK: 54+

Error symptoms:

  • App crashes during BLE device disconnect
  • Native crashes that bypass JavaScript error handling
  • Crashes occur specifically when using characteristic.monitor() operations

Root Cause

The issue is in BlePlxModule.java where 17 instances of error handling use:

safePromise.reject(null, errorConverter.toJs(error));

React Native's PromiseImpl.reject() requires non-null error codes, but the BLE library passes null during disconnect scenarios.

GitHub References:

  • Issue: https://github.com/dotintent/react-native-ble-plx/issues/1303
  • PR: https://github.com/dotintent/react-native-ble-plx/pull/1304

Solution

Use patch-package to fix the native Android code by replacing all null error codes with proper error code names.

Step 1: Install patch-package

npm install --save-dev patch-package

Step 2: Add postinstall script

Add this to your package.json:

{
  "scripts": {
    "postinstall": "patch-package"
  }
}

Step 3: Create the patch file

  1. Create a patches directory in your project root
  2. Create patches/react-native-ble-plx+3.5.0.patch with the following content:
diff --git a/node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java b/node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java
index 1234567..abcdefg 100644
--- a/node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java
+++ b/node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java
@@ -168,7 +168,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
     }, new OnErrorCallback() {
       @Override
       public void onError(BleError error) {
-        safePromise.reject(null, errorConverter.toJs(error));
+        safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     });
   }
@@ -187,7 +187,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
     }, new OnErrorCallback() {
       @Override
       public void onError(BleError error) {
-        safePromise.reject(null, errorConverter.toJs(error));
+        safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     });
   }
@@ -318,7 +318,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       });
   }
@@ -338,7 +338,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       });
   }
@@ -358,7 +358,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       });
   }
@@ -422,7 +422,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       });
   }
@@ -442,7 +442,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       });
   }
@@ -483,7 +483,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       });
   }
@@ -627,7 +627,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       }
     );
@@ -654,7 +654,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       }
     );
@@ -680,7 +680,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       });
   }
@@ -706,7 +706,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       }
     );
@@ -732,7 +732,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       }
     );
@@ -757,7 +757,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       }
     );
@@ -788,7 +788,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       }
     );
@@ -818,7 +818,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       }
     );
@@ -848,7 +848,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule {
       }, new OnErrorCallback() {
         @Override
         public void onError(BleError error) {
-          safePromise.reject(null, errorConverter.toJs(error));
+          safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
         }
       }
     );

Step 4: Apply the patch

# Apply the patch immediately
npx patch-package

# For Expo projects - rebuild native code (required for Android changes)
npx expo prebuild --clean

# For standard React Native projects
npx react-native run-android

How It Works

Automatic Application

The patch is automatically applied via the postinstall script:

  • Every npm install - Patch gets applied automatically
  • Every npm ci - Same automatic application
  • After dependency updates - Ensures the patch persists
  • On new team member setup - Automatically applies when they install dependencies

What Gets Fixed

The patch fixes 17 instances in BlePlxModule.java where:

// BEFORE (causes crashes):
safePromise.reject(null, errorConverter.toJs(error));

// AFTER (fixed):
safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));

This affects all BLE monitoring operations:

  • characteristic.monitor() calls
  • Button press monitoring
  • Sensor data monitoring
  • Battery level monitoring
  • Distance/RSSI monitoring

Verification

Test the fix:

  1. Connect to a BLE device
  2. Start characteristic monitoring
  3. Manually disconnect the device (turn off, move out of range, etc.)
  4. App should handle disconnect gracefully without crashing

Expected behavior:

  • ✅ No native crashes during disconnect
  • ✅ Clean disconnect handling
  • ✅ Proper error reporting with valid error codes
  • ✅ App continues to function normally

Benefits

  1. Automatic - No manual intervention needed after setup
  2. Persistent - Survives dependency updates and team onboarding
  3. Version-controlled - The patch file is committed to your repo
  4. Reliable - Works consistently across different environments
  5. Minimal - Only fixes the specific issue without other modifications
  6. Production-ready - Safe for production use

Compatibility

  • ✅ React Native 0.81.4+
  • ✅ Expo SDK 54+
  • ✅ react-native-ble-plx 3.2.1, 3.5.0
  • ✅ Android (primary fix)
  • ✅ iOS (no changes needed, works normally)

This fix has been tested and verified to resolve BLE disconnect crashes in production React Native applications. `

thenjneer avatar Oct 03 '25 23:10 thenjneer

react-native-ble-plx+3.5.0.patch

For anyone having this issue, here's a working patch^

And here's a writeup (courtesy of mr Claude) for how to use it. @aliberski @angelos3lex this fix is tested working if you plan on updating:

`# Fix for react-native-ble-plx Android Crash on Disconnect

Problem

React Native apps using react-native-ble-plx crash on Android when BLE devices disconnect unexpectedly. This is caused by Promise.reject(null) calls in the native Android code, which crashes modern React Native versions that require non-null error codes.

Affected versions:

  • react-native-ble-plx: 3.2.1, 3.5.0
  • React Native: 0.81.4+
  • Expo SDK: 54+

Error symptoms:

  • App crashes during BLE device disconnect
  • Native crashes that bypass JavaScript error handling
  • Crashes occur specifically when using characteristic.monitor() operations

Root Cause

The issue is in BlePlxModule.java where 17 instances of error handling use:

safePromise.reject(null, errorConverter.toJs(error)); React Native's PromiseImpl.reject() requires non-null error codes, but the BLE library passes null during disconnect scenarios.

GitHub References:

Solution

Use patch-package to fix the native Android code by replacing all null error codes with proper error code names.

Step 1: Install patch-package

npm install --save-dev patch-package

Step 2: Add postinstall script

Add this to your package.json:

{ "scripts": { "postinstall": "patch-package" } }

Step 3: Create the patch file

  1. Create a patches directory in your project root
  2. Create patches/react-native-ble-plx+3.5.0.patch with the following content:

diff --git a/node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java b/node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java index 1234567..abcdefg 100644 --- a/node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java +++ b/node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java @@ -168,7 +168,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {

  •    safePromise.reject(null, errorConverter.toJs(error));
    
  •    safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
     }
    
    }); } @@ -187,7 +187,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •    safePromise.reject(null, errorConverter.toJs(error));
    
  •    safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
     }
    
    }); } @@ -318,7 +318,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     });
    
    } @@ -338,7 +338,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     });
    
    } @@ -358,7 +358,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     });
    
    } @@ -422,7 +422,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     });
    
    } @@ -442,7 +442,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     });
    
    } @@ -483,7 +483,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     });
    
    } @@ -627,7 +627,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     }
    
    ); @@ -654,7 +654,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     }
    
    ); @@ -680,7 +680,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     });
    
    } @@ -706,7 +706,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     }
    
    ); @@ -732,7 +732,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     }
    
    ); @@ -757,7 +757,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     }
    
    ); @@ -788,7 +788,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     }
    
    ); @@ -818,7 +818,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     }
    
    ); @@ -848,7 +848,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {
  •      safePromise.reject(null, errorConverter.toJs(error));
    
  •      safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));
       }
     }
    
    );

Step 4: Apply the patch

Apply the patch immediately

npx patch-package

For Expo projects - rebuild native code (required for Android changes)

npx expo prebuild --clean

For standard React Native projects

npx react-native run-android

How It Works

Automatic Application

The patch is automatically applied via the postinstall script:

  • Every npm install - Patch gets applied automatically
  • Every npm ci - Same automatic application
  • After dependency updates - Ensures the patch persists
  • On new team member setup - Automatically applies when they install dependencies

What Gets Fixed

The patch fixes 17 instances in BlePlxModule.java where:

// BEFORE (causes crashes): safePromise.reject(null, errorConverter.toJs(error));

// AFTER (fixed): safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); This affects all BLE monitoring operations:

  • characteristic.monitor() calls
  • Button press monitoring
  • Sensor data monitoring
  • Battery level monitoring
  • Distance/RSSI monitoring

Verification

Test the fix:

  1. Connect to a BLE device
  2. Start characteristic monitoring
  3. Manually disconnect the device (turn off, move out of range, etc.)
  4. App should handle disconnect gracefully without crashing

Expected behavior:

  • ✅ No native crashes during disconnect
  • ✅ Clean disconnect handling
  • ✅ Proper error reporting with valid error codes
  • ✅ App continues to function normally

Benefits

  1. Automatic - No manual intervention needed after setup
  2. Persistent - Survives dependency updates and team onboarding
  3. Version-controlled - The patch file is committed to your repo
  4. Reliable - Works consistently across different environments
  5. Minimal - Only fixes the specific issue without other modifications
  6. Production-ready - Safe for production use

Compatibility

  • ✅ React Native 0.81.4+
  • ✅ Expo SDK 54+
  • ✅ react-native-ble-plx 3.2.1, 3.5.0
  • ✅ Android (primary fix)
  • ✅ iOS (no changes needed, works normally)

This fix has been tested and verified to resolve BLE disconnect crashes in production React Native applications. `

This patch is working, thank you so much

Mr-KID-github avatar Oct 09 '25 11:10 Mr-KID-github

This patch worked perfectly, thank you @thenjneer!

mrtomhoward avatar Oct 09 '25 11:10 mrtomhoward

Works for me as well! ❤️

potocpav avatar Oct 09 '25 14:10 potocpav

Thank you!

lamebear avatar Oct 10 '25 04:10 lamebear

Thank you for taking the effort of digging into this issue which is bugging me just now. I guess SafePromise isn't so safe after all... ;-)

I'd like to add something here: SafePromise.reject() delegates to com.facebook.react.bridge.Promise.reject() and this is were the exception is thrown. So we need to look further. A simple search for .reject(null in my IDE revealed 35 occurrences in total. All of those can be replayed with .reject(error.errorCode.name().

Hope we are safe again with SafePromise after all.

shynst avatar Oct 13 '25 16:10 shynst

Hello,

If you having issues applying the patch with patch-package, please follow these steps:

  1. Create a file fix-ble.js at your project root:
  2. Copy-paste the below content
  3. Add "scripts": { "postinstall": "node fix-ble.js" } to your package.json
  4. Run rm -rf node_modules && yarn install (or equivalent with npm)
  5. Run grep "safePromise.reject(error.errorCode.name()" node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java | wc -l to check if the patch has been applied => you should have 17
  6. Rebuild on android cd android && ./gradlew generateCodegenArtifactsFromSchema && ./gradlew clean && ./gradlew assembleDebug && cd .. && yarn android

Here is the content for fix-ble.js:

const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, 'node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java');

let content = fs.readFileSync(filePath, 'utf8');
content = content.replace(/safePromise\.reject\(null, errorConverter\.toJs\(error\)\);/g, 'safePromise.reject(error.errorCode.name(), errorConverter.toJs(error));');

fs.writeFileSync(filePath, content, 'utf8');

jorismangel avatar Oct 21 '25 12:10 jorismangel

Note that (until this gets merged) you can simply install b1naryth1ef's fork via:

npm install --save https://github.com/b1naryth1ef/react-native-ble-plx.git#fix/promise-rejection

prathje avatar Nov 18 '25 14:11 prathje

Hitting this too - hopefully this will be upstreamed soon. (Until then - maybe a note on the README about known issues with the latest rn versions?)

Zee2 avatar Dec 23 '25 23:12 Zee2