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)
@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:
catchError wasn't added later.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.
Most helpful comment
There is no assert.
It's unfortunately not that easy to add an
asserttocatchErrorwithout too many false positives. Futures may get reused, in which case they are (on purpose) already completed, or they might just be completed immediately.What would have helped (I guess): a zone that just logs, and then triggers your
catchError.In this example you will see the "zone error" message, but you still get the normal
catchErrornotification. This would have probably led you on the right tracks.