Skip to content

Conversation

@sweetbot
Copy link

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, ...) with safePromise.reject(error.errorCode.name(), ...)
  • Replace promise.reject(null, ...) with promise.reject(error.errorCode.name(), ...)
  • Replace promise.reject(null, ...) with promise.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)
                                                                                                    

- Fix all 35 instances of null error codes in promise.reject calls
- Include both safePromise.reject and promise.reject fixes
- Handle both error and bleError parameter naming consistently
- Address React Native 0.80.0+ null-safety requirements
- Resolves crashes in BLE operations with RN 0.81.4+

Resolves: dotintent#1310
@sweetbot sweetbot force-pushed the fix-react-native-080-comprehensive-fix branch from 3a4dcb7 to 235d9fa Compare October 26, 2025 20:54
@Yusuf-Munir
Copy link

Hopefully this gets pulled in soon, I used patch-package to fix the current version.

Thanks for making the pull request :)

@edwinw6
Copy link

edwinw6 commented Oct 30, 2025

After upgrading our app to Expo 54 we ran into this issue; and I can confirm the suggested patch resolved it for us.

@alariois
Copy link

This patch fixed the issue for us also.

Would be great if this could be merged to the official library.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants