It’s impossible to catch errors for futures that complete before callbacks are registered

If a future completes with an error before a catchError callback is registered, it’s seemingly impossible to catch that error.

If this is in fact the intended behavior, I don’t see this documented anywhere. Adding something akin to “Unlike Future.then, callbacks registered with Future.catchError will not be executed if they are registered after the future has already completed” to the documentation for Future would be helpful.

See this example program:

import 'dart:async';

void main() {
  try {
    completeError();
  } catch (error) {
    // Doesn't  run.
    print('caught in main');
  }
}

Future completeError() async {
  var completer;
  try {
    completer = new Completer<bool>();
    completer.completeError('error');
  } catch(error) {
    // Doesn't run.
    print('caught');
  }

  try {
    await new Future.delayed(const Duration(seconds: 1));
    completer.future.catchError((error) {
      // Doesn't run.
      print('caught error: $error');
    }); 
  } catch (e) {
    // Doesn't run.
    print('caught');
  }
}

Output:

Unhandled exception:
error
#0      _rootHandleUncaughtError.<anonymous closure> (dart:async/zone.dart:1146)
#1      _microtaskLoop (dart:async/schedule_microtask.dart:41)
#2      _startMicrotaskLoop (dart:async/schedule_microtask.dart:50)
#3      _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:96)
#4      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:149)

Author: Fantashit

1 thought on “It’s impossible to catch errors for futures that complete before callbacks are registered

  1. There is no assert.

    It’s unfortunately not that easy to add an assert to catchError without too many false positives. Futures may get reused, in which case they are (on purpose) already completed, or they might just be completed immediately.

      void foo() {
        if (canBeShortCut) return new Future.value(null);
        return doSomeAsynchronousOperation();
      }
    
      foo().catchError(...);  // Assert should not trigger.

    What would have helped (I guess): a zone that just logs, and then triggers your catchError.

    import 'dart:async';
    
    main() {
      runZoned(() async {
        var completer = new Completer();
        completer.completeError("foo");
        await new Future.delayed(const Duration(milliseconds: 20));
        completer.future.catchError(print);
      }, onError: (e) { print("zone error: $e"); });
    }

    In this example you will see the “zone error” message, but you still get the normal catchError notification. This would have probably led you on the right tracks.

Comments are closed.