fix: comprehensive React Native 0.80.0+ compatibility fix
Problem
React Native 0.80.0 was the first release to include the PromiseImpl null-safety changes.
The React Native Commit in April 2025 "fix nullsafe FIXMEs for PromiseImpl.java and mark nullsafe" by Gijs Weterings 4c8ea858a53 - fix nullsafe FIXMEs for PromiseImpl.java and mark nullsafe
What This Means: React Native 0.80.0+ enforces non-null error codes in PromiseImpl.reject() React Native 0.79.x and earlier allowed null as the error code The change added @Nullsafe(Nullsafe.Mode.LOCAL) annotation to PromiseImpl class This triggers NullPointerException when null is passed as the error code
Root Cause
React Native 0.80.0+ added @Nullsafe(Nullsafe.Mode.LOCAL) annotation to PromiseImpl class, which enforces non-null error codes at runtime.
This bluetooth library was calling in a lot of places:
safePromise.reject(null, errorConverter.toJs(error));
But React Native 0.80.0+ now requires:
promise.reject(String code, Object error); // code cannot be null
React Native 0.80.0 was the first release to include this change that broke some features of this library
Impact
It fixes an urgent compatibility issue impacting a wide range of users forced to move to React Native 0.81.4+ (especially Expo SDK 54+ projects). Updating to Expo 54 makes it impossible to use this library without crash, since Expo 54 is only compatible with React Native 0.81.4+.
This will fix issues for Ledger reactive native Device Management SDK and other reactive native SDKs which use this library as the transport layer.
Related Issue that I reported 2 days ago: #1310 - Android crash in BLETransport APIs when React Native version is 0.81.4
Solution
Replace all null error codes with proper BleErrorCode enum values using error.errorCode.name().
- Fix all instances of null error codes in promise.reject calls
- Include both safePromise.reject and promise.reject fixes
- Address React Native 0.80.0+ null-safety requirements
Java code changes in this PR
In android/src/main/java/com/bleplx/BlePlxModule.java
- Replace
safePromise.reject(null, ...)withsafePromise.reject(error.errorCode.name(), ...) - Replace
promise.reject(null, ...)withpromise.reject(error.errorCode.name(), ...) - Replace
promise.reject(null, ...)withpromise.reject(bleError.errorCode.name(), ...)
Testing results
- ✅ Works with React Native 0.79.6
- ✅ Works with React Native 0.81.4+
- ✅ No more crashes on disconnect operations
- ✅ Maintains backward compatibility
Error Log
Some more details of Native Android logs here:
09:44:45.917 3915-4176 ReactNativeJS com.ranitomeya.ledgerexpodemo I 🔌 Starting Manual disconnect - setting disconnecting state...
2025-10-26 09:44:45.918 3915-4176 ReactNativeJS com.ranitomeya.ledgerexpodemo I Manual disconnect - disconnecting state set successfully
2025-10-26 09:44:45.918 3915-4176 ReactNativeJS com.ranitomeya.ledgerexpodemo I Manual disconnect - disconnecting...
2025-10-26 09:44:45.933 3915-4176 ReactNativeJS com.ranitomeya.ledgerexpodemo I Manual disconnect - session validated, proceeding with disconnect
2025-10-26 09:44:45.935 3915-4176 ReactNativeJS com.ranitomeya.ledgerexpodemo I Manual disconnect - calling dmk.disconnect with sessionId: 5e0cb30d-13fa-49ad-bde2-225fd04639f6
2025-10-26 09:44:45.941 3915-4176 ReactNativeJS com.ranitomeya.ledgerexpodemo I Manual disconnect - dmk.disconnect completed successfully
2025-10-26 09:44:45.952 3915-4542 BluetoothGatt com.ranitomeya.ledgerexpodemo D cancelOpen() - device: XX:XX:XX:XX:76:22
2025-10-26 09:44:45.956 3915-3927 BluetoothGatt com.ranitomeya.ledgerexpodemo D onClientConnectionState() - status=0 connected=false device=XX:XX:XX:XX:76:22
2025-10-26 09:44:45.956 3915-3927 BluetoothGatt com.ranitomeya.ledgerexpodemo D unregisterApp()
2025-10-26 09:44:45.958 3915-3927 BluetoothGatt com.ranitomeya.ledgerexpodemo D setCharacteristicNotification() - uuid: 13d63400-2c97-6004-0001-4c6564676572 enable: false
2025-10-26 09:44:45.960 3915-4542 BluetoothGatt com.ranitomeya.ledgerexpodemo D close()
2025-10-26 09:44:45.963 3915-4670 AndroidRuntime com.ranitomeya.ledgerexpodemo E FATAL EXCEPTION: RxComputationThreadPool-4
Process: com.ranitomeya.ledgerexpodemo, PID: 3915
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 0 (GATT_SUCCESS)
at com.polidea.rxandroidble2.internal.connection.RxBleGattCallback$2.onConnectionStateChange(RxBleGattCallback.java:81)
at android.bluetooth.BluetoothGatt$GattCallback.lambda$onClientConnectionState$3(BluetoothGatt.java:426)
at android.bluetooth.BluetoothGatt$GattCallback.$r8$lambda$g0wdVhK6mBSTzAIOY21zUwHI--c(Unknown Source:0)
at android.bluetooth.BluetoothGatt$GattCallback$$ExternalSyntheticLambda10.run(D8$$SyntheticClass:0)
at android.bluetooth.BluetoothGatt$GattCallback.runOrQueueCallback(BluetoothGatt.java:277)
at android.bluetooth.BluetoothGatt$GattCallback.onClientConnectionState(BluetoothGatt.java:422)
at android.bluetooth.IBluetoothGattCallback$Stub.onTransact(IBluetoothGattCallback.java:209)
at android.os.Binder.execTransactInternal(Binder.java:1426)
at android.os.Binder.execTransact(Binder.java:1365)
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:817)
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)
E 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 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:817)
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)
Hopefully this gets pulled in soon, I used patch-package to fix the current version.
Thanks for making the pull request :)
After upgrading our app to Expo 54 we ran into this issue; and I can confirm the suggested patch resolved it for us.
This patch fixed the issue for us also.
Would be great if this could be merged to the official library.
It would be good to look at existing open issues (#1303) and PRs (#1304) in the future so you don't replicate other peoples efforts.
Any updates? We really stuck on our update to react native 0.80.0 / expo54
I hate to say it but I did a fork of this library because I had to fix the same problem... I just reviewed your PR and it's identical to one of the problem I fixed @ArtemFokin @b1naryth1ef @alariois
If you guys want to collaborate.
https://github.com/sfourdrinier/react-native-ble-plx
@sfourdrinier I tried your fork and cant find the types (like BleManager) in code, import doesnt work, you should give a check.