Description
To reproduce dart run
the following code:
import 'dart:async';
void main() {
final Completer<int> completer = Completer<int>();
final Future<int?> future = completer.future; // cast Future<int> to Future<int?>
future.catchError((_) {
return null; // this only looks correct because the future is allowed to return null
});
completer.completeError('Oops!');
}
Expected result
The stack trace should point to the code containing the programming error, i.e. (future.catchError
). Consider the following non-async code containing a similar error:
void main() {
final List<int> list = <int>[1, 2, 3];
final List<int?> nullList = list;
nullList.add(null);
}
The result is:
Unhandled exception:
type 'Null' is not a subtype of type 'int' of 'value'
#0 List.add (dart:core-patch/growable_array.dart)
#1 main (file:///usr/local/google/home/yjbanov/code/tmp/null_future.dart:4:12)
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)
The stack trace points to the line 4 in the file null_future.dart
that I wrote, which is helpful.
Actual result
Unhandled exception:
Invalid argument(s) (onError): The error handler of Future.catchError must return a value of the future's type
#0 _FutureListener.handleError (dart:async/future_impl.dart:194:7)
#1 Future._propagateToListeners.handleError (dart:async/future_impl.dart:779:47)
#2 Future._propagateToListeners (dart:async/future_impl.dart:800:13)
#3 Future._completeError (dart:async/future_impl.dart:610:5)
#4 Future._asyncCompleteError.<anonymous closure> (dart:async/future_impl.dart:666:7)
#5 _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
#6 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
#7 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:122:13)
#8 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:193:5)
This stack trace is not helpful at all. The best method I could find is pause on exceptions in the debugger, wait until it pauses on this line, then eval $T
. This gives you the type that it expected. Then you grep your source code for futures and completers that use this type (hopefully there aren't too many places) and try to spot the bug.
As a concrete example, here's the bug in the Flutter Web engine tool, which was a conspiracy of three separate lines of code:
Completer<BrowserEngine>
instantiated.- The completer's
.future
is implicitly cast toFuture<BrowserEngine?>
. - A
catchError
attempts to return null.
No matter what Dart program you write you see the exact same stack trace, making it very hard to debug.