Mobile Device Environment
- Device: iPhone (any); reproduces on simulator too
- OS: iOS 17/18
Application Environment
- React Native version: v0.83.1
- RN Bluetooth Classic version: v1.73.0-rc.12 — code is identical in
1.73.0-rc.17 (latest) and on main
Describe the bug
cancelDiscovery() is declared as returning Promise<boolean>, but on iOS it throws
synchronously before any promise exists (src/BluetoothModule.ts:239-242 on main):
cancelDiscovery(): Promise<boolean> {
if (Platform.OS == 'ios') throw new Error('Method not implemented.');
return this._nativeModule.cancelDiscovery();
}
Because no promise is returned, the natural defensive call fails to protect anything:
// looks safe, is not: on iOS `.catch()` is never reached
await RNBluetoothClassic.cancelDiscovery().catch(() => false);
The throw propagates out of the caller's expression instead of becoming a rejection, so callers
who correctly handle "this platform can't do that" still break.
This is inconsistent within the module itself. Of the nine methods guarded with
Method not implemented., three reject (they're async, so the throw is converted) and six throw
synchronously:
Rejects — .catch() works |
Throws synchronously — .catch() never attaches |
accept, pairDevice, startDiscovery |
cancelDiscovery, cancelAccept, openBluetoothSettings, requestBluetoothEnabled, setBluetoothAdapterName, unpairDevice |
To Reproduce
On iOS:
import RNBluetoothClassic from 'react-native-bluetooth-classic';
// 1. rejection handler does not run; the error escapes synchronously
await RNBluetoothClassic.cancelDiscovery().catch(() => false);
// 2. same for the try/catch equivalent placed around an await
try {
await RNBluetoothClassic.cancelDiscovery();
} catch {
/* this DOES catch, because the throw happens inside the try */
}
Case 1 is the one that bites: .catch() on the returned value can only run if a value is
returned.
Expected behavior
An unsupported platform should produce a rejected promise (or resolve as a no-op, per the
docstring's "if discovery was already stopped, this will end gracefully by resolving"), matching the
declared Promise<boolean> signature and the behaviour of accept / pairDevice / startDiscovery.
Either of these would do it:
async cancelDiscovery(): Promise<boolean> {
if (Platform.OS == 'ios') throw new Error('Method not implemented.');
return this._nativeModule.cancelDiscovery();
}
cancelDiscovery(): Promise<boolean> {
if (Platform.OS == 'ios') return Promise.reject(new Error('Method not implemented.'));
return this._nativeModule.cancelDiscovery();
}
Additional context
In a production React Native app this silently disabled all External Accessory connections on
iOS. Our shared Classic connect path cancels discovery before dialling (necessary on Android, where
connecting mid-discovery is unreliable), guarded with .catch(() => false). On iOS the throw escaped
that guard, propagated out of the connect helper, and rejected every connect() — accessories were
discovered and listed normally, but connecting was impossible. It took a while to trace precisely
because the guard looks platform-neutral and the failure surfaces far from this line.
Not asking for iOS implementations of these methods — External Accessory has no discovery phase, so
"not implemented" is the correct answer. The request is only that it be delivered as a rejection so
.catch() and .then(onFulfilled, onRejected) work as the type signature implies.
Happy to open a PR normalising all six if you'd like it.
Mobile Device Environment
Application Environment
1.73.0-rc.17(latest) and onmainDescribe the bug
cancelDiscovery()is declared as returningPromise<boolean>, but on iOS it throwssynchronously before any promise exists (
src/BluetoothModule.ts:239-242onmain):Because no promise is returned, the natural defensive call fails to protect anything:
The throw propagates out of the caller's expression instead of becoming a rejection, so callers
who correctly handle "this platform can't do that" still break.
This is inconsistent within the module itself. Of the nine methods guarded with
Method not implemented., three reject (they'reasync, so the throw is converted) and six throwsynchronously:
.catch()works.catch()never attachesaccept,pairDevice,startDiscoverycancelDiscovery,cancelAccept,openBluetoothSettings,requestBluetoothEnabled,setBluetoothAdapterName,unpairDeviceTo Reproduce
On iOS:
Case 1 is the one that bites:
.catch()on the returned value can only run if a value isreturned.
Expected behavior
An unsupported platform should produce a rejected promise (or resolve as a no-op, per the
docstring's "if discovery was already stopped, this will end gracefully by resolving"), matching the
declared
Promise<boolean>signature and the behaviour ofaccept/pairDevice/startDiscovery.Either of these would do it:
Additional context
In a production React Native app this silently disabled all External Accessory connections on
iOS. Our shared Classic connect path cancels discovery before dialling (necessary on Android, where
connecting mid-discovery is unreliable), guarded with
.catch(() => false). On iOS the throw escapedthat guard, propagated out of the connect helper, and rejected every
connect()— accessories werediscovered and listed normally, but connecting was impossible. It took a while to trace precisely
because the guard looks platform-neutral and the failure surfaces far from this line.
Not asking for iOS implementations of these methods — External Accessory has no discovery phase, so
"not implemented" is the correct answer. The request is only that it be delivered as a rejection so
.catch()and.then(onFulfilled, onRejected)work as the type signature implies.Happy to open a PR normalising all six if you'd like it.