Sdk: It's impossible to catch errors for futures that complete before callbacks are registered

Created on 22 Mar 2017  路  5Comments  路  Source: dart-lang/sdk

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)
area-library library-async

Most helpful comment

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.

All 5 comments

@leiboldm It's not clear what you are trying to do or what you expect.

try { ... } catch { ... } only catches _synchronous_ errors. The exception is using await:

try {
 await runAFuture();
} catch (e) {
 // Same as runAFuture.catchError(e)
}

The following catches just fine, for example:

import 'dart:async';

void main() {
  errorInASecond()
    .catchError((error) {
      print('1. catchError(): ${error == 'ERROR'}');
    })
    .whenComplete(() async {
      try {
        await errorInASecond();
      } catch (e) {
        print('2. try/catch with await: ${e == 'ERROR'}');
      }
    });
}

Future errorInASecond() async {
  await new Future.delayed(const Duration(seconds: 1));
  var completer = new Completer();
  completer.completeError('ERROR');
  return completer.future;
}

This is working as intended and the consequence of several design decisions:

  • adding an error handler to a future in the same microtask should always catch the error. This means that errors can never be caught synchronously. It would be (generally) impossible to know if a catchError wasn't added later.
  • errors are never dropped. If a future completes with an error and there is no listener, then that error must be reported.

This behavior is documented on the Future class itself: https://api.dartlang.org/stable/1.22.1/dart-async/Future-class.html

If a future does not have a successor when it completes with an error, it forwards the error message to the global error-handler. This behavior makes sure that no error is silently dropped. However, it also means that error handlers should be installed early, so that they are present as soon as a future is completed with an error.

I will add a similar text to .catchError and .then.

@matanlurey

I filed this issue because I was trying to add error handling code to part of large project and nothing I did was successful in catching the error. It wasn't until 2 or 3 hours of spinning my wheels that I figured out the reason my error handler wasn't getting called was because it was being registered after the error had already occurred. I didn't even consider this as a potential issue because the onValue callback ran just fine when the future completed with a value so I assumed onError and catchError would behave the same when the future completed with an error. It seems to me that if you register an error handler on a future that has already completed, it should at minimum log a warning saying that the error handler has no chance of being executed because the future is already completed.

@floitschG Is there value in an assert() that catchError doesn't happen after execution?

I imagine this is a big breaking change now, though.

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

nex3 picture nex3  路  3Comments

brooth picture brooth  路  3Comments

rinick picture rinick  路  3Comments

55555Mohit55555 picture 55555Mohit55555  路  3Comments

gspencergoog picture gspencergoog  路  3Comments