Asynchrony

Domains: Dart

DO

DO test for Future<T> when disambiguating a FutureOr<T> whose type argument could be Object.

Before you can do anything useful with a FutureOr<T>, you typically need to do an is check to see if you have a Future<T> or a bare T. If the type argument is some specific type as in FutureOr<int>, it doesn’t matter which test you use, is int or is Future<int>. Either works because those two types are disjoint.

However, if the value type is Object or a type parameter that could possibly be instantiated with Object, then the two branches overlap. Future<Object> itself implements Object, so is Object or is T where T is some type parameter that could be instantiated with Object returns true even when the object is a future. Instead, explicitly test for the Future case:

Future<T> logValue<T>(FutureOr<T> value) async {
  if (value is Future<T>) {
    var result = await value;
    print(result);
    return result;
  } else {
    print(value);
    return value as T;
  }
}
Future<T> logValue<T>(FutureOr<T> value) async {
  if (value is T) {
    print(value);
    return value;
  } else {
    var result = await value;
    print(result);
    return result;
  }
}

In the bad example, if you pass it a Future<Object>, it incorrectly treats it like a bare, synchronous value.

PREFER

PREFER async/await over using raw futures.

Asynchronous code is notoriously hard to read and debug, even when using a nice abstraction like futures. The async/await syntax improves readability and lets you use all of the Dart control flow structures within your async code.

Future<int> countActivePlayers(String teamName) async {
  try {
    var team = await downloadTeam(teamName);
    if (team == null) return 0;

    var players = await team.roster;
    return players.where((player) => player.isActive).length;
  } catch (e) {
    log.error(e);
    return 0;
  }
}
Future<int> countActivePlayers(String teamName) {
  return downloadTeam(teamName).then((team) {
    if (team == null) return Future.value(0);

    return team.roster.then((players) {
      return players.where((player) => player.isActive).length;
    });
  }).catchError((e) {
    log.error(e);
    return 0;
  });
}

CONSIDER

CONSIDER using higher-order methods to transform a stream.

This parallels the above suggestion on iterables. Streams support many of the same methods and also handle things like transmitting errors, closing, etc. correctly

AVOID

AVOID using Completer directly.

Many people new to asynchronous programming want to write code that produces a future. The constructors in Future don’t seem to fit their need so they eventually find the Completer class and use that.

Future<bool> fileContainsBear(String path) {
  var completer = Completer<bool>();

  File(path).readAsString().then((contents) {
    completer.complete(contents.contains('bear'));
  });

  return completer.future;
}

Completer is needed for two kinds of low-level code: new asynchronous primitives, and interfacing with asynchronous code that doesn’t use futures. Most other code should use async/await or Future.then(), because they’re clearer and make error handling easier.

Future<bool> fileContainsBear(String path) {
  return File(path).readAsString().then((contents) {
    return contents.contains('bear');
  });
}
Future<bool> fileContainsBear(String path) async {
  var contents = await File(path).readAsString();
  return contents.contains('bear');
}

DON'T

DON’T use async when it has no useful effect.

It’s easy to get in the habit of using async on any function that does anything related to asynchrony. But in some cases, it’s extraneous. If you can omit the async without changing the behavior of the function, do so.

Future afterTwoThings(Future first, Future second) {
  return Future.wait([first, second]);
}
Future afterTwoThings(Future first, Future second) async {
  return Future.wait([first, second]);
}

Cases where async is useful include:

  • You are using await. (This is the obvious one.)

  • You are returning an error asynchronously. async and then throw is shorter than return Future.error(...).

  • You are returning a value and you want it implicitly wrapped in a future. async is shorter than Future.value(...).

Future usesAwait(Future later) async {
  print(await later);
}

Future asyncError() async {
  throw 'Error!';
}

Future asyncValue() async => 'value';

Similar pages

Page structure
Terms

int

String

bool

Functions

Methods

Dart

Types

Constructor