Skip to content

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

Closed

Description

@LohenHM

Prerequisites

  • I checked the documentation and FAQ without finding a solution
  • 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

Activity

  1. josmithua commented on Sep 16, 2025

    @josmithua
    Contributor

    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));
             }
           }
         );
  2. karlvonladach commented on Sep 17, 2025

    @karlvonladach

    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.

  3. fatihkayan20 commented on Sep 17, 2025

    @fatihkayan20

    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)

  4. josmithua commented on Sep 17, 2025

    @josmithua
    Contributor

    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.

  5. thenjneer commented on Oct 3, 2025

    @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.
    `

  6. Mr-KID-github commented on Oct 9, 2025

    @Mr-KID-github

    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

  7. mrtomhoward commented on Oct 9, 2025

    @mrtomhoward

    This patch worked perfectly, thank you @thenjneer!

  8. potocpav commented on Oct 9, 2025

    @potocpav

    Works for me as well! ❤️

  9. lamebear commented on Oct 10, 2025

    @lamebear

    Thank you!

  10. shynst commented on Oct 13, 2025

    @shynst

    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.

  11. jorismangel commented on Oct 21, 2025

    @jorismangel

    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');
    
    
  12. prathje commented on Nov 18, 2025

    @prathje

    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
  13. Zee2 commented on Dec 23, 2025

    @Zee2

    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?)

  14. aliberski commented on Mar 9, 2026

    @aliberski
    Collaborator

    This is already fixed in the latest version 3.5.1 (PR for reference)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions