Skip to content

Future.catchError unsafe if you don't know the runtimeType of the Future it is called on #51248

@christopherfujino

Description

@christopherfujino

Update April 3, 2024

The Future.onError extension method now solves this problem--that is, this code works without throwing an Error:

Future<void> main() async {
  await func().onError(handler);
}

Null handler(_, __) => null;

Future<Object?> func() => Future<bool>.error('oops');

Consider the following code:

Future<void> main() async {
  await func().catchError(handler);
}

Null handler(_, __) => null;

Future<Object?> func() => Future<bool>.error('oops');

This code appears to be statically correct. Because the return type of handler satisfies the return type of func(), it looks like this code should work. However, at runtime, the .catchError method will actually be called on a Future<bool>, and now our handler function is no longer a valid callback to Future<bool>.catchError(), and we get an ArgumentError:

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:181: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:575: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:123:13)
#8      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:5)

Even though the onError extension method features a strictly-typed callback, it still leads to a runtime-only error:

Future<void> main() async {
  await func().onError(handler);
}

Null handler(_, __) => null;

Future<Object?> func() => Future<bool>.error('oops');

Leads to the same stacktrace.

Interestingly, passing our handler to the onError parameter of .then() does not have the runtime error:

Future<void> main() async {
  await func().then(
    (Object? obj) => obj,
    onError: handler,
  );
}

Null handler(_, __) => null;

Future<Object?> func() => Future<bool>.error('oops');

This has lead to a series of very difficult to track down crashes in the Flutter CLI tool (especially since the stacktrace is opaque), such as flutter/flutter#114031

In this particular case, we were doing essentially:

import 'dart:io' as io;

Future<void> main() async {
  final process = await io.Process.start('interactive-script.sh', <String>[]);
  // stdin is an IOSink
  process.stdin
      // IOSink.addStream's static type is Future<dynamic> Function(Stream<List<int>>)
      // however, the actual runtimeType of what is returned is `Future<Socket>`
      .addStream(io.stdin)
      .catchError((dynamic err, __) => print(err));
}

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