Futures, async and await
Dart runs your code on a single thread with an event loop. Nothing you write runs in parallel with anything else you write, which means you never need a lock
- and it also means that any code which blocks stops everything.
A Future<T> is a value that is not here yet. It is not a thread; it is a
promise that something will complete, with a value or with an error.
Future<String> fetchName() async {
await Future<void>.delayed(const Duration(milliseconds: 50));
return 'Ada';
}
void main() async {
print('before');
final name = await fetchName();
print('got $name');
print('after');
}
async marks a function that may suspend; await is where it suspends. The
function returns a Future immediately, and the rest of it runs later, when the
awaited future completes.
What await actually does to the order
Future<void> main() async {
print('1 sync');
Future<void>.delayed(Duration.zero, () => print('4 event queue'));
scheduleMicrotaskLike();
final value = await Future<int>.value(99);
print('5 after await: $value');
print('6 last');
}
void scheduleMicrotaskLike() {
Future<void>.microtask(() => print('3 microtask'));
print('2 sync inside the call');
}
Two queues decide that order. Microtasks - which is where a completed Future
resumes - all run before the next event. Timers and I/O go on the event queue
and wait their turn. It is why a tight loop of microtasks can starve a timer.
Errors travel through futures
An async function that throws returns a failed future. await inside a try
turns it back into a normal exception, which is the whole reason await is
nicer than callbacks.
Future<int> risky(int n) async {
if (n < 0) throw ArgumentError.value(n, 'n', 'must not be negative');
return n * 2;
}
Future<void> main() async {
print(await risky(21));
try {
await risky(-1);
} on ArgumentError catch (e) {
print('caught: ${e.message}');
}
// Without await, the error has nowhere to go but the zone's error handler -
// which in a server is the difference between a logged 500 and a silent one.
await risky(-1).then((v) => print(v)).catchError((Object e) {
print('handled in catchError: $e');
});
}
Doing several things at once
Awaiting in sequence takes as long as the sum. Future.wait starts everything
and takes as long as the slowest.
Future<String> load(String name, int ms) async {
await Future<void>.delayed(Duration(milliseconds: ms));
return name;
}
Future<void> main() async {
final clock = Stopwatch()..start();
// Sequential: 60 + 60 + 60.
await load('a', 60);
await load('b', 60);
await load('c', 60);
print('sequential: ${clock.elapsedMilliseconds >= 180}');
clock.reset();
// Concurrent: all three are already running by the time we wait.
final results = await Future.wait([
load('a', 60),
load('b', 60),
load('c', 60),
]);
print('concurrent: ${results.join(",")} in about 60ms: '
'${clock.elapsedMilliseconds < 180}');
}
Future.wait fails as soon as any of its futures fails. If you want every
result regardless, wrap each one so its failure becomes a value.
Future<String> maybe(String name, {bool fail = false}) async {
await Future<void>.delayed(const Duration(milliseconds: 10));
if (fail) throw StateError('$name failed');
return name;
}
Future<void> main() async {
final outcomes = await Future.wait([
maybe('a'),
maybe('b', fail: true),
maybe('c'),
].map((f) => f.then<String>((v) => 'ok:$v').catchError((Object e) => 'err:$e')));
outcomes.forEach(print);
}
Timeouts, because something that never completes is the worst case
// Future and Stream are in dart:core, but TimeoutException is not.
import 'dart:async';
Future<void> main() async {
Future<String> slow() async {
await Future<void>.delayed(const Duration(seconds: 5));
return 'eventually';
}
try {
await slow().timeout(const Duration(milliseconds: 100));
} on TimeoutException {
print('gave up after 100ms');
}
// Or supply a value instead of throwing.
final value = await slow()
.timeout(const Duration(milliseconds: 100), onTimeout: () => 'default');
print(value);
}
What to take away
- One thread, one event loop: no locks, and nothing may block.
asyncreturns a future immediately;awaitis where the function suspends.- Microtasks (resumed futures) run before events (timers, I/O).
- An error in an async function becomes a failed future;
awaitinsidetrycatches it. awaitin sequence costs the sum;Future.waitcosts the slowest.Future.waitfails fast - convert failures to values if you need every result.- Put a
timeouton anything that talks to the outside world.