🐛 Android: App crashes on device disconnect when using monitorCharacteristicForDevice()
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
- Connect to a BLE peripheral device.
- Start monitoring a characteristic using
device.monitorCharacteristicForDevice(...). - Force the device to disconnect (e.g., by turning the peripheral off or moving it out of range).
- 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
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));
}
}
);
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.
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)
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.
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
- Create a
patchesdirectory in your project root - Create
patches/react-native-ble-plx+3.5.0.patchwith 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:
- Connect to a BLE device
- Start characteristic monitoring
- Manually disconnect the device (turn off, move out of range, etc.)
- 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
- Automatic - No manual intervention needed after setup
- Persistent - Survives dependency updates and team onboarding
- Version-controlled - The patch file is committed to your repo
- Reliable - Works consistently across different environments
- Minimal - Only fixes the specific issue without other modifications
- 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. `
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-plxcrash on Android when BLE devices disconnect unexpectedly. This is caused byPromise.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()operationsRoot Cause
The issue is in
BlePlxModule.javawhere 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 passesnullduring disconnect scenarios.GitHub References:
- Issue: 🐛 Android: App crashes on device disconnect when using monitorCharacteristicForDevice() #1303
- PR: Provide error code on promise rejections (fixes #1303) #1304
Solution
Use
patch-packageto fix the native Android code by replacing allnullerror 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
- Create a
patchesdirectory in your project root- Create
patches/react-native-ble-plx+3.5.0.patchwith 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));
}); } @@ -187,7 +187,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); }
safePromise.reject(null, errorConverter.toJs(error));
}); } @@ -318,7 +318,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); }
safePromise.reject(null, errorConverter.toJs(error));
} @@ -338,7 +338,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } });
safePromise.reject(null, errorConverter.toJs(error));
} @@ -358,7 +358,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } });
safePromise.reject(null, errorConverter.toJs(error));
} @@ -422,7 +422,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } });
safePromise.reject(null, errorConverter.toJs(error));
} @@ -442,7 +442,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } });
safePromise.reject(null, errorConverter.toJs(error));
} @@ -483,7 +483,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } });
safePromise.reject(null, errorConverter.toJs(error));
} @@ -627,7 +627,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } });
safePromise.reject(null, errorConverter.toJs(error));
); @@ -654,7 +654,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } }
safePromise.reject(null, errorConverter.toJs(error));
); @@ -680,7 +680,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } }
safePromise.reject(null, errorConverter.toJs(error));
} @@ -706,7 +706,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } });
safePromise.reject(null, errorConverter.toJs(error));
); @@ -732,7 +732,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } }
safePromise.reject(null, errorConverter.toJs(error));
); @@ -757,7 +757,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } }
safePromise.reject(null, errorConverter.toJs(error));
); @@ -788,7 +788,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } }
safePromise.reject(null, errorConverter.toJs(error));
); @@ -818,7 +818,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(error)); } }
safePromise.reject(null, errorConverter.toJs(error));
); @@ -848,7 +848,7 @@ public class BlePlxModule extends ReactContextBaseJavaModule { }, new OnErrorCallback() { @Override public void onError(BleError error) {safePromise.reject(error.errorCode.name(), errorConverter.toJs(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
postinstallscript:
- 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.javawhere:// 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:
- Connect to a BLE device
- Start characteristic monitoring
- Manually disconnect the device (turn off, move out of range, etc.)
- 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
- Automatic - No manual intervention needed after setup
- Persistent - Survives dependency updates and team onboarding
- Version-controlled - The patch file is committed to your repo
- Reliable - Works consistently across different environments
- Minimal - Only fixes the specific issue without other modifications
- 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
This patch worked perfectly, thank you @thenjneer!
Works for me as well! ❤️
Thank you!
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.
Hello,
If you having issues applying the patch with patch-package, please follow these steps:
- Create a file fix-ble.js at your project root:
- Copy-paste the below content
- Add
"scripts": { "postinstall": "node fix-ble.js" }to your package.json - Run
rm -rf node_modules && yarn install(or equivalent with npm) - Run
grep "safePromise.reject(error.errorCode.name()" node_modules/react-native-ble-plx/android/src/main/java/com/bleplx/BlePlxModule.java | wc -lto check if the patch has been applied => you should have 17 - 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');
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
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?)