Skip to content

🐛 NullPointerException in BlePlxModule when BLE device disconnects with GATT timeout #1323

Description

@Victorvikson1996

Prerequisites

  • I checked the documentation and FAQ without finding a solution
  • I checked to make sure that this issue has not already been filed

Expected Behavior

Here's what you should report to react-native-ble-plx on GitHub:

Bug Report Template
Title: NullPointerException in BlePlxModule when BLE device disconnects with GATT timeout

Description
When a BLE device disconnects with GATT status 8 (GATT_CONN_TIMEOUT or GATT_INSUF_AUTHORIZATION), the native Android module crashes with a NullPointerException because the error code passed to PromiseImpl.reject() is null.

Environment
react-native-ble-plx version: 3.5.0
React Native version: 0.81.5
Platform: Android 10 (device: Redmi 9C / M2006C3MG)
New Architecture: Enabled
Stack Trace
Root Cause
In BlePlxModule.java line 65, the RxJava global error handler calls promise.reject() with an error that has a null code. The React Native bridge requires a non-null code parameter.

Suggested Fix
In BlePlxModule.java, add null-safety when rejecting promises:

GitHub Issue URL: https://github.com/dotintent/react-native-ble-plx/issues/new

Would you like me to help you search if this issue already exists before filing a new one?

Current Behavior

react-native-ble-plx version: 3.5.0
React Native version: 0.81.5
Platform: Android 10 (device: Redmi 9C / M2006C3MG)
New Architecture: Enabled

Library version

react-native-ble-plx version: 3.5.0

Device

Platform: Android 10 (device: Redmi 9C / M2006C3MG)

Environment info

nfo Fetching system and libraries information...
System:
  OS: macOS 26.2
  CPU: (8) arm64 Apple M1
  Memory: 146.42 MB / 8.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 22.11.0
    path: /Users/chukwuebuka/.asdf/installs/nodejs/22.11.0/bin/node
  Yarn:
    version: 1.22.22
    path: /Users/chukwuebuka/node_modules/.bin/yarn
  npm:
    version: 11.4.2
    path: /Users/chukwuebuka/.asdf/installs/nodejs/22.11.0/bin/npm
  Watchman:
    version: 2025.03.03.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.16.2
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 25.1
      - iOS 26.1
      - macOS 26.1
      - tvOS 26.1
      - visionOS 26.1
      - watchOS 26.1
  Android SDK: Not Found
IDEs:
  Android Studio: 2025.2 AI-252.27397.103.2522.14514259
  Xcode:
    version: 26.1.1/17B100
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 21.0.8
    path: /usr/bin/javac
  Ruby:
    version: 3.4.4
    path: /opt/homebrew/opt/ruby/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 19.1.0
    wanted: 19.1.0
  react-native:
    installed: 0.81.5
    wanted: 0.81.5
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: true
  newArchEnabled: true

(node:42291) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
info React Native v0.83.0 is now available (your project is running on v0.81.5).
info Changelog: https://github.com/facebook/react-native/releases/tag/v0.83.0
info Diff: https://react-native-community.github.io/upgrade-helper/?from=0.81.5
info For more info, check out "https://reactnative.dev/docs/upgrading?os=macos".

Humax-Installer-Mobile-App on  fix/ble-unsubscribe-reboot-guard [!?] took 5.0s 
➜

Steps to reproduce

Connect to a BLE device
Move the device out of range or cause a connection timeout
Device disconnects with GATT status 8
App crashes with NullPointerException
Expected Behavior
The library should handle disconnection errors gracefully without crashing, even when the error code is null.

Formatted code sample or link to a repository

import {
  BleManager,
  Device,
  Characteristic,
  State,
  BleError as NativeBleError
} from 'react-native-ble-plx';
import { Platform, PermissionsAndroid } from 'react-native';
import { BLEError, BLEErrorCode, BLEDevice } from './types';
import { BLECommands } from './commands';
import { BLEParser } from './parser';

/**
 * BLE Manager Service
 * Handles all Bluetooth Low Energy operations for HUMAX devices
 */
export class BLEManagerService {
  private manager: BleManager;
  private device: Device | null = null;
  private writeCharacteristic: Characteristic | null = null;
  private notifyCharacteristic: Characteristic | null = null;
  private commands: BLECommands;
  private parser: BLEParser;
  private responseCallbacks: Map<string, (data: number[]) => void> = new Map();
  private disconnectCallbacks: Map<
    string,
    (error: NativeBleError | null | undefined) => void
  > = new Map();
  private isConnected: boolean = false;
  private isRebooting: boolean = false; // Flag to suppress callbacks during reboot

  constructor() {
    this.manager = new BleManager();
    this.commands = new BLECommands();
    this.parser = new BLEParser();
  }

  /**
   * Request Bluetooth permissions (Android)
   */
  async requestPermissions(): Promise<boolean> {
    if (Platform.OS === 'android') {
      if (Platform.Version >= 31) {
        // Android 12+ - Check permissions first before requesting
        const permissions = [
          PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
          PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
          PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
        ];

        // Check if all permissions are already granted
        const checks = await Promise.all(
          permissions.map((permission) => PermissionsAndroid.check(permission))
        );

        if (checks.every((result) => result === true)) {
          // All permissions already granted, no need to request again
          return true;
        }

        // Some permissions missing, request them
        const granted = await PermissionsAndroid.requestMultiple(permissions);

        return Object.values(granted).every(
          (status) => status === PermissionsAndroid.RESULTS.GRANTED
        );
      } else {
        // Android 11 and below - Check permission first
        const hasPermission = await PermissionsAndroid.check(
          PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
        );

        if (hasPermission) {
          // Permission already granted
          return true;
        }

        // Request permission
        const granted = await PermissionsAndroid.request(
          PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
        );

        return granted === PermissionsAndroid.RESULTS.GRANTED;
      }
    }

    return true; // iOS handles permissions automatically
  }

  /**
   * Check if Bluetooth is powered on
   */
  async isBluetoothEnabled(): Promise<boolean> {
    const state = await this.manager.state();
    return state === State.PoweredOn;
  }

  /**
   * Listen to Bluetooth state changes
   * @param callback Callback when Bluetooth state changes
   * @param emitCurrentState Whether to emit the current state immediately
   * @returns Subscription object with remove() method
   */
  onStateChange(
    callback: (state: State) => void,
    emitCurrentState: boolean = true
  ) {
    return this.manager.onStateChange(callback, emitCurrentState);
  }

  /**
   * Start scanning for BEV devices
   * @param onDeviceFound Callback when a BEV device is found
   * @param timeout Scan timeout in milliseconds (default: 15 minutes)
   */
  async startScan(
    onDeviceFound: (device: BLEDevice) => void,
    timeout: number = 900000 // 15 minutes
  ): Promise<void> {
    const hasPermission = await this.requestPermissions();
    if (!hasPermission) {
      throw new BLEError(
        'Bluetooth permissions not granted',
        BLEErrorCode.PERMISSION_DENIED
      );
    }

    const isEnabled = await this.isBluetoothEnabled();
    if (!isEnabled) {
      throw new BLEError('Bluetooth is turned off', BLEErrorCode.BLUETOOTH_OFF);
    }

    this.manager.startDeviceScan(
      null,
      { allowDuplicates: false },
      (error, device) => {
        if (error) {
          console.error('Scan error:', error);
          return;
        }

        if (device?.name && device.name.startsWith('BEV')) {
          // Avoid strict typing issues by attaching serialNumber to the native Device
          const bleDevice = device as unknown as BLEDevice;
          (bleDevice as any).serialNumber = device.name.substring(3); // Remove 'BEV' prefix
          try {
            onDeviceFound(bleDevice);
          } catch (cbErr) {
            console.error('Error in onDeviceFound callback:', cbErr);
          }
        }
      }
    );

    // Auto-stop after timeout
    setTimeout(() => this.stopScan(), timeout);
  }

  /**
   * Stop scanning for devices
   */
  async stopScan(): Promise<void> {
    await this.manager.stopDeviceScan();
  }

  /**
   * Connect to a BLE device
   * @param deviceId Device ID to connect to
   * @returns Connected device
   */
  async connect(deviceId: string): Promise<Device> {
    try {
      console.log('🔵 BLEManager: Connecting to device:', deviceId);
      this.device = await this.manager.connectToDevice(deviceId, {
        timeout: 30000 // 30 second timeout
      });
      console.log('🔵 BLEManager: Device connected, discovering services...');

      await this.device.discoverAllServicesAndCharacteristics();
      console.log('🔵 BLEManager: Services discovered');

      // Request MTU
      try {
        await this.device.requestMTU(223);
        console.log('🔵 BLEManager: MTU requested successfully');
      } catch (error) {
        console.warn('MTU request failed (non-critical):', error);
      }

      // Find and setup characteristics
      console.log('🔵 BLEManager: Setting up characteristics...');
      await this.setupCharacteristics();
      console.log('🔵 BLEManager: Characteristics setup complete');
      console.log(
        '🔵 BLEManager: writeCharacteristic:',
        !!this.writeCharacteristic
      );
      console.log(
        '🔵 BLEManager: notifyCharacteristic:',
        !!this.notifyCharacteristic
      );

      // mark connected and setup disconnection monitoring
      this.isConnected = true;
      console.log('🔵 BLEManager: Connection fully established');

      try {
        this.device.onDisconnected((error, device) => {
          console.log('Device disconnected:', device?.id, error?.message);
          this.handleDisconnection(error);
        });
      } catch (e) {
        // not critical on some platforms
        console.warn('Failed to register onDisconnected handler:', e);
      }

      return this.device;
    } catch (error) {
      console.error('🔴 BLEManager: Connection failed:', error);
      throw new BLEError(
        'Failed to connect to device',
        BLEErrorCode.CONNECTION_FAILED,
        error
      );
    }
  }

  /**
   * Setup BLE characteristics (write, notify)
   */
  private async setupCharacteristics(): Promise<void> {
    if (!this.device) {
      throw new BLEError('No device connected', BLEErrorCode.DISCONNECTED);
    }

    const services = await this.device.services();

    for (const service of services) {
      // Filter out invalid service UUIDs
      if (service.uuid.length < 10) continue;

      const characteristics = await service.characteristics();

      for (const char of characteristics) {
        // Find writable characteristic
        if (
          !this.writeCharacteristic &&
          (char.isWritableWithResponse || char.isWritableWithoutResponse)
        ) {
          this.writeCharacteristic = char;
        }

        // Find notifiable characteristic
        if (!this.notifyCharacteristic && char.isNotifiable) {
          this.notifyCharacteristic = char;

          // Setup notification monitoring
          await this.notifyCharacteristic.monitor((error, characteristic) => {
            if (error) {
              // Expected errors during disconnect/reboot - log as info, not error
              const message = error.message?.toLowerCase() || '';
              if (
                this.isRebooting ||
                !this.isConnected ||
                message.includes('disconnected') ||
                message.includes('cancelled') ||
                message.includes('canceled')
              ) {
                console.log('Notification stopped (device disconnected)');
              } else {
                console.warn('Notification error:', error.message);
              }
              return;
            }

            if (characteristic?.value) {
              const data = this.base64ToArray(characteristic.value);
              this.handleNotification(data);
            }
          });
        }
      }
    }

    if (!this.writeCharacteristic || !this.notifyCharacteristic) {
      throw new BLEError(
        'Required characteristics not found',
        BLEErrorCode.CONNECTION_FAILED
      );
    }
  }

  /**
   * Handle incoming notifications from device
   */
  private handleNotification(data: number[]): void {
    try {
      // CRITICAL: Ignore ALL notifications during reboot
      if (this.isRebooting) {
        console.log('Ignoring notification - device is rebooting');
        return;
      }

      // Ignore notifications if device is already disconnected
      if (!this.isConnected) {
        console.log('Ignoring notification - device already disconnected');
        return;
      }

      // Parse the response
      const parsed = this.parser.parseResponse(data);

      // Call any registered callbacks (guarded)
      const callbacks = Array.from(this.responseCallbacks.entries());
      callbacks.forEach(([key, callback]) => {
        try {
          callback(data);
        } catch (cbErr) {
          console.error(`Error in response callback ${key}:`, cbErr);
        }
      });

      // Log for debugging
      if (parsed) {
        console.log('BLE Response:', parsed);
      }
    } catch (err) {
      console.error('Unhandled error in handleNotification:', err);
    }
  }

  /**
   * Handle device disconnection and notify listeners
   */
  private handleDisconnection(error: NativeBleError | null | undefined): void {
    try {
      // Safely extract error message (handle null/undefined)
      const errorMessage = error?.message ?? 'No error details';
      console.log('Handling disconnection, error:', errorMessage);

      // CRITICAL: Suppress ALL callbacks during reboot (expected disconnect)
      if (this.isRebooting) {
        console.log(
          'Ignoring disconnection callbacks - device is rebooting (expected)'
        );
        this.isConnected = false;
        return;
      }

      // Prevent duplicate processing
      if (!this.isConnected) {
        console.log('Already disconnected, skipping handleDisconnection');
        return;
      }

      this.isConnected = false;

      // Notify all registered disconnection callbacks
      const callbacks = Array.from(this.disconnectCallbacks.entries());
      console.log(`Notifying ${callbacks.length} disconnection callbacks`);
      callbacks.forEach(([key, callback]) => {
        try {
          console.log(`Calling disconnection callback: ${key}`);
          // Always pass a valid error object, never undefined
          callback(error ?? null);
        } catch (err) {
          console.error(`Error in disconnection callback ${key}:`, err);
        }
      });

      // cleanup
      this.device = null;
      this.writeCharacteristic = null;
      this.notifyCharacteristic = null;
      this.responseCallbacks.clear();
      // Note: do not clear disconnectCallbacks here so consumers can remove them explicitly

      console.log('Disconnection handling complete');
    } catch (err) {
      console.error('Critical error in handleDisconnection:', err);
      this.isConnected = false;
      this.device = null;
      this.writeCharacteristic = null;
      this.notifyCharacteristic = null;
      this.responseCallbacks.clear();
    }
  }

  /**
   * Write command to device
   * @param command Command bytes to write
   */
  async writeCommand(command: number[]): Promise<void> {
    console.log(
      '🔵 BLEManager: writeCommand called, device:',
      !!this.device,
      'writeChar:',
      !!this.writeCharacteristic,
      'isConnected:',
      this.isConnected
    );

    if (!this.device || !this.writeCharacteristic) {
      console.error(
        '🔴 BLEManager: Cannot write - device:',
        !!this.device,
        'writeCharacteristic:',
        !!this.writeCharacteristic
      );
      throw new BLEError(
        'Device not connected or characteristic not found',
        BLEErrorCode.DISCONNECTED
      );
    }

    try {
      const base64Data = this.arrayToBase64(command);
      await this.writeCharacteristic.writeWithResponse(base64Data);

      // Small delay between commands
      await this.delay(100);
    } catch (error) {
      // If the write failed because the device disconnected, translate to DISCONNECTED
      const message =
        error instanceof Error
          ? error.message.toLowerCase()
          : String(error).toLowerCase();
      if (
        message.includes('disconnected') ||
        message.includes('not connected')
      ) {
        this.isConnected = false;
        throw new BLEError(
          'Device disconnected during write',
          BLEErrorCode.DISCONNECTED,
          error
        );
      }

      throw new BLEError(
        'Failed to write command',
        BLEErrorCode.WRITE_FAILED,
        error
      );
    }
  }

  /**
   * Write a command that will cause the device to immediately disconnect (e.g. reset).
   * This method attempts a best-effort write and does not throw on expected disconnects.
   */
  async writeCommandAndExpectDisconnect(command: number[]): Promise<void> {
    if (!this.device || !this.writeCharacteristic) {
      throw new BLEError(
        'Device not connected or characteristic not found',
        BLEErrorCode.DISCONNECTED
      );
    }

    try {
      const base64Data = this.arrayToBase64(command);

      if (this.writeCharacteristic.isWritableWithoutResponse) {
        try {
          await this.writeCharacteristic.writeWithoutResponse(base64Data);
        } catch (err) {
          // ignore - device may drop connection
          console.log(
            'writeWithoutResponse failed (expected for disconnect):',
            err
          );
        }
      } else {
        // Fallback to writeWithResponse but race with a short timeout
        const writePromise =
          this.writeCharacteristic.writeWithResponse(base64Data);
        const timeout = new Promise((_, reject) =>
          setTimeout(() => reject(new Error('Write timeout')), 1000)
        );
        try {
          await Promise.race([writePromise, timeout]);
        } catch (err) {
          // Timeout / error is expected when device disconnects
          console.log(
            'Write timed out or failed during reset (expected):',
            err
          );
        }
      }
    } catch (err) {
      console.log('Error during reset write (expected):', err);
    }
  }

  /**
   * Read RSSI (signal strength)
   */
  async readRSSI(): Promise<number> {
    if (!this.device) {
      throw new BLEError('No device connected', BLEErrorCode.DISCONNECTED);
    }

    try {
      const device = await this.device.readRSSI();
      return device.rssi || -100;
    } catch (error) {
      throw new BLEError(
        'Failed to read RSSI',
        BLEErrorCode.READ_FAILED,
        error
      );
    }
  }

  /**
   * Disconnect from device
   */
  async disconnect(): Promise<void> {
    if (this.device) {
      try {
        await this.device.cancelConnection();
      } catch (error) {
        console.warn('Disconnect error (non-critical):', error);
      }

      this.device = null;
      this.writeCharacteristic = null;
      this.notifyCharacteristic = null;
      this.responseCallbacks.clear();
    }
  }

  /**
   * Register callback for BLE responses
   */
  onResponse(key: string, callback: (data: number[]) => void): void {
    this.responseCallbacks.set(key, callback);
  }

  /**
   * Unregister response callback
   */
  offResponse(key: string): void {
    this.responseCallbacks.delete(key);
  }

  /** Register callback for device disconnection */
  onDisconnected(
    key: string,
    callback: (error: NativeBleError | null | undefined) => void
  ): void {
    this.disconnectCallbacks.set(key, callback);
  }

  /** Unregister disconnection callback */
  offDisconnected(key: string): void {
    this.disconnectCallbacks.delete(key);
  }

  /**
   * Set reboot mode - suppresses all BLE callbacks during device reboot
   * MUST be called BEFORE sending reset command
   */
  setRebootMode(isRebooting: boolean): void {
    console.log(`Setting reboot mode: ${isRebooting}`);
    this.isRebooting = isRebooting;

    // NOTE: We intentionally do NOT clear device/characteristic state here
    // when exiting reboot mode, because by that point we've already
    // successfully reconnected and need those references for subsequent commands.
    // The state is cleared in clearCharacteristicsAfterReset() instead.
  }

  /**
   * Clear characteristics after reset - call AFTER sending reset command
   */
  clearCharacteristicsAfterReset(): void {
    console.log('Clearing characteristics after reset command sent');

    // Just clear references - don't call cancelConnection as device is already rebooting
    this.writeCharacteristic = null;
    this.notifyCharacteristic = null;
    this.responseCallbacks.clear();
    // Note: Keep device reference so handleDisconnection can still check isRebooting
    // but clear it in handleDisconnection after the callback fires
    this.isConnected = false;

    console.log('Characteristics cleared successfully');
  }

  /**
   * Check if currently in reboot mode
   */
  isInRebootMode(): boolean {
    return this.isRebooting;
  }

  /**
   * Get command builder instance
   */
  getCommands(): BLECommands {
    return this.commands;
  }

  /**
   * Get parser instance
   */
  getParser(): BLEParser {
    return this.parser;
  }

  /**
   * Get current connected device
   */
  getConnectedDevice(): Device | null {
    return this.device;
  }

  /**
   * Cleanup and destroy manager
   */
  destroy(): void {
    this.manager.destroy();
  }

  /**
   * Wait until Bluetooth state becomes PoweredOn or timeout occurs
   * @param timeout Timeout in milliseconds (default: 30000)
   */
  async waitForBluetoothOn(timeout: number = 30000): Promise<void> {
    try {
      const state = await this.manager.state();
      if (state === State.PoweredOn) return;

      await new Promise<void>((resolve, reject) => {
        let timer: NodeJS.Timeout | null = null;

        const sub = this.manager.onStateChange((newState) => {
          if (newState === State.PoweredOn) {
            try {
              sub.remove();
            } catch (e) {}
            // Clear the timeout to prevent memory leak
            if (timer) clearTimeout(timer);
            resolve();
          }
        }, true);

        timer = setTimeout(() => {
          try {
            sub.remove();
          } catch (e) {}
          reject(
            new BLEError(
              'Bluetooth did not power on in time',
              BLEErrorCode.UNKNOWN
            )
          );
        }, timeout);
      });
    } catch (err) {
      throw err instanceof BLEError
        ? err
        : new BLEError(
            'Failed waiting for Bluetooth',
            BLEErrorCode.UNKNOWN,
            err
          );
    }
  }

  // Utility methods
  private base64ToArray(base64: string): number[] {
    const binary = atob(base64);
    const array: number[] = [];
    for (let i = 0; i < binary.length; i++) {
      array.push(binary.charCodeAt(i));
    }
    return array;
  }

  private arrayToBase64(array: number[]): string {
    const binary = String.fromCharCode(...array);
    return btoa(binary);
  }

  private delay(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}

// Singleton instance
let bleManagerInstance: BLEManagerService | null = null;

export const getBLEManager = (): BLEManagerService => {
  if (!bleManagerInstance) {
    bleManagerInstance = new BLEManagerService();
  }
  return bleManagerInstance;
};

Relevant log output

There are 4 chained exceptions in this event.


CompositeException
2 exceptions occurred. 
mechanism
UncaughtExceptionHandler
handled
false
io.reactivex.internal.subscribers.LambdaSubscriber in onError at line 82
io.reactivex.internal.operators.flowable.FlowableDoOnEach$DoOnEachSubscriber in onError at line 111
io.reactivex.internal.operators.flowable.FlowableDoOnLifecycle$SubscriptionLambdaSubscriber in onError at line 85
io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber in checkTerminated at line 209
io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber in runAsync at line 399
io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber in run at line 176
io.reactivex.internal.schedulers.ScheduledRunnable in run at line 66
io.reactivex.internal.schedulers.ScheduledRunnable in call at line 57
java.util.concurrent.FutureTask in run at line 266
java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask in run at line 301
java.util.concurrent.ThreadPoolExecutor in runWorker at line 1167
java.util.concurrent.ThreadPoolExecutor$Worker in run at line 641
java.lang.Thread in run at line 919

CompositeException$CompositeExceptionCausalChain
Chain of Causes for CompositeException In Order Received =>
mechanism
chained
java.lang.ThreadGroup in uncaughtException at line 1073
java.lang.ThreadGroup in uncaughtException at line 1068
com.bleplx.BlePlxModule in lambda$new$0 at line 65
com.bleplx.BlePlxModule$$ExternalSyntheticLambda0 in accept
io.reactivex.plugins.RxJavaPlugins in onError at line 373
io.reactivex.internal.subscribers.LambdaSubscriber in onError at line 82
io.reactivex.internal.operators.flowable.FlowableDoOnEach$DoOnEachSubscriber in onError at line 111
io.reactivex.internal.operators.flowable.FlowableDoOnLifecycle$SubscriptionLambdaSubscriber in onError at line 85
io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber in checkTerminated at line 209
io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber in runAsync at line 399
io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber in run at line 176
io.reactivex.internal.schedulers.ScheduledRunnable in run at line 66
io.reactivex.internal.schedulers.ScheduledRunnable in call at line 57
java.util.concurrent.FutureTask in run at line 266
java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask in run at line 301
java.util.concurrent.ThreadPoolExecutor in runWorker at line 1167
java.util.concurrent.ThreadPoolExecutor$Worker in run at line 641
java.lang.Thread in run at line 919

BleDisconnectedException
Disconnected from MAC='XX:XX:XX:XX:XX:XX' with status 8 (GATT_INSUF_AUTHORIZATION or GATT_CONN_TIMEOUT)
mechanism
chained
com.polidea.rxandroidble2.internal.connection.RxBleGattCallback$2 in onConnectionStateChange at line 81
android.bluetooth.BluetoothGatt$1$4 in run at line 272
android.bluetooth.BluetoothGatt in runOrQueueCallback at line 780
android.bluetooth.BluetoothGatt in access$200 at line 41
android.bluetooth.BluetoothGatt$1 in onClientConnectionState at line 267
android.bluetooth.IBluetoothGattCallback$Stub in onTransact at line 192
android.os.Binder in execTransactInternal at line 1021
android.os.Binder in execTransact at line 994

NullPointerException
Parameter specified as non-null is null: method com.facebook.react.bridge.PromiseImpl.reject, parameter code
mechanism
chained
com.facebook.react.bridge.PromiseImpl in reject at line 2
com.bleplx.utils.SafePromise in reject at line 25
com.bleplx.BlePlxModule$45 in onError at line 851
com.bleplx.adapter.utils.SafeExecutor in error at line 30
com.bleplx.adapter.BleModule in lambda$safeMonitorCharacteristicForDevice$45 at line 1485
com.bleplx.adapter.BleModule in $r8$lambda$JVuqIGnSfaxzZFLoyHm91xQUhdI
com.bleplx.adapter.BleModule$$ExternalSyntheticLambda3 in accept
io.reactivex.internal.subscribers.LambdaSubscriber in onError at line 79
io.reactivex.internal.operators.flowable.FlowableDoOnEach$DoOnEachSubscriber in onError at line 111
io.reactivex.internal.operators.flowable.FlowableDoOnLifecycle$SubscriptionLambdaSubscriber in onError at line 85
io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber in checkTerminated at line 209
io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber in runAsync at line 399
io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber in run at line 176
io.reactivex.internal.schedulers.ScheduledRunnable in run at line 66
io.reactivex.internal.schedulers.ScheduledRunnable in call at line 57
java.util.concurrent.FutureTask in run at line 266
java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask in run at line 301
java.util.concurrent.ThreadPoolExecutor in runWorker at line 1167
java.util.concurrent.ThreadPoolExecutor$Worker in run at line 641
java.lang.Thread in run at line 919

Additional information

NullPointerException: Parameter specified as non-null is null:
method com.facebook.react.bridge.PromiseImpl.reject, parameter code

com.bleplx.BlePlxModule in lambda$new$0 at line 65
com.bleplx.BlePlxModule$$ExternalSyntheticLambda0 in accept
io.reactivex.plugins.RxJavaPlugins in onError at line 373
io.reactivex.internal.subscribers.LambdaSubscriber in onError at line 82
...

Caused by:
BleDisconnectedException: Disconnected from MAC='XX:XX:XX:XX:XX:XX'
with status 8 (GATT_INSUF_AUTHORIZATION or GATT_CONN_TIMEOUT)

Root Cause
In BlePlxModule.java line 65, the RxJava global error handler calls promise.reject() with an error that has a null code. The React Native bridge requires a non-null code parameter.

Steps to Reproduce
Connect to a BLE device
Move the device out of range or cause a connection timeout
Device disconnects with GATT status 8
App crashes with NullPointerException
Expected Behavior
The library should handle disconnection errors gracefully without crashing, even when the error code is null.

Suggested Fix
In BlePlxModule.java, add null-safety when rejecting promises:

String errorCode = error.getCode() != null ? error.getCode() : "BLE_ERROR";
promise.reject(errorCode, error.getMessage(), error);

Activity

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