Skip to content

Casting Future<Foo> to Future<Foo?> leads to hard-to-debug errors too easily #47308

Open
@yjbanov

Description

@yjbanov

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:

No matter what Dart program you write you see the exact same stack trace, making it very hard to debug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-core-librarySDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries.library-asynctype-enhancementA request for a change that isn't a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions