Cannot catch an asynchronous exception

212

That connect method from the package you're using is poorly written. You're not doing anything wrong, whoever wrote that code did. If you poke around the GitHub Issues sections of that repository you'll find quite a few issues and pull requests related to this problem like this and the issues/PR it links.

The code in a timer callback exists outside of the function where the timer is instantiated. It's impossible to directly catch an error thrown in a timer callback.

If you want a timeout, don't use the functionality provided by this package, use the native Dart timeout function.

try {
  await (device as BluetoothDevice).connect().timeout(Duration(seconds: 1));
} catch (o) {
  print("caught ${o.runtimeType}");
}
Share:
212
ReubenBeeler
Author by

ReubenBeeler

Updated on November 23, 2022

Comments

  • ReubenBeeler
    ReubenBeeler over 1 year

    Here is a simple example of my problem:

    import 'dart:async';
    
    void main() {
      try {
        Timer(Duration(seconds: 1), () {
          throw TimeoutException('1 second has expired');
        });
      } catch (o) {
        print("caught ${o.runtimeType}");
      }
    }
    

    I imagine the problem is that the timer finishes counting down after the try-catch block terminates, considering that the countdown is asynchronous and initializing the Timer was successful. How can I catch the exception without changing the callback function of the Timer?

    In my specific case, I am using flutter_blue and having trouble with the async method BluetoothDevice#connect().

    /// Establishes a connection to the Bluetooth Device.
    Future<void> connect({
      Duration? timeout,
      bool autoConnect = true,
    }) async {
      var request = protos.ConnectRequest.create()
        ..remoteId = id.toString()
        ..androidAutoConnect = autoConnect;
    
      Timer? timer;
      if (timeout != null) {
        timer = Timer(timeout, () {
          disconnect();
          throw TimeoutException('Failed to connect in time.', timeout);
        });
      }
    
      await FlutterBlue.instance._channel
          .invokeMethod('connect', request.writeToBuffer());
    
      await state.firstWhere((s) => s == BluetoothDeviceState.connected);
    
      timer?.cancel();
    
      return;
    }
    

    I called the method like so:

    try {
      await (device as BluetoothDevice).connect(timeout: Duration(seconds: 1));
    } catch (o) {
      print("caught ${o.runtimeType}");
    }
    

    Considering I wait for BluetoothDevice#connect() with await and timer is cancelled upon a successful connection (at the end of the method) with timer?.cancel();, I do not know why the try-catch is not catching the following TimeoutException:

    E/flutter ( 3710): [ERROR:flutter/lib/ui/ui_dart_state.cc(199)] Unhandled Exception: TimeoutException after 0:00:01.000000: Failed to connect in time.
    E/flutter ( 3710): #0      BluetoothDevice.connect.<anonymous closure> (package:flutter_blue/src/bluetooth_device.dart:33:9)
    E/flutter ( 3710): #1      _rootRun (dart:async/zone.dart:1346:47)
    E/flutter ( 3710): #2      _CustomZone.run (dart:async/zone.dart:1258:19)
    E/flutter ( 3710): #3      _CustomZone.runGuarded (dart:async/zone.dart:1162:7)
    E/flutter ( 3710): #4      _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1202:23)
    E/flutter ( 3710): #5      _rootRun (dart:async/zone.dart:1354:13)
    E/flutter ( 3710): #6      _CustomZone.run (dart:async/zone.dart:1258:19)
    E/flutter ( 3710): #7      _CustomZone.bindCallback.<anonymous closure> (dart:async/zone.dart:1186:23)
    E/flutter ( 3710): #8      Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
    E/flutter ( 3710): #9      _Timer._runTimers (dart:isolate-patch/timer_impl.dart:395:19)
    E/flutter ( 3710): #10     _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:426:5)
    E/flutter ( 3710): #11     _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
    E/flutter ( 3710): 
    

    I see that the stack trace starts at BluetoothDevice#connect(), but I am not sure what to do about it.

    I also tried calling then((_) {}, (o) => print("caught ${o.runtimeType}")) on the Future<void> returned by BluetoothDevice#connect() and then waiting for it inside the try-catch, yet I had no success.

    Any ideas?

  • ReubenBeeler
    ReubenBeeler over 2 years
    Thank you. I found a solution to my specific case, which is using BluetoothDevice#disconnect() because it apparently still works while asynchronously executing BluetoothDevice#connect(). Other than that, however, this answer is exactly what I was looking for.
  • Christopher Moore
    Christopher Moore over 2 years
    @ReubenBeeler If my answer helped, please accept it by clicking the checkmark to the left of it.