08 · Async Basics¶
Why async matters¶
Dart is single-threaded by default, but it's not blocking — I/O-bound work
(network requests, file reads, timers) runs asynchronously via an event loop,
so your program can keep doing other things while it waits. The Future type
represents a value that will be available later.
Future<T>¶
A Future<T> represents a computation that will eventually produce a value of
type T (or fail with an error).
Future<String> fetchGreeting() {
return Future.delayed(const Duration(seconds: 1), () => 'Hello, world!');
}
void main() {
print('Fetching...');
fetchGreeting().then((greeting) {
print(greeting);
});
print('This prints before the greeting!');
}
// Fetching...
// This prints before the greeting!
// Hello, world! <- appears ~1 second later
Notice the order of prints: .then() schedules a callback for later, so
main keeps running past it immediately — this is the essence of
non-blocking async code.
async and await¶
await pauses execution within an async function until a Future
completes, without blocking the rest of the program. It reads like
synchronous code while still being fully non-blocking.
Future<String> fetchGreeting() {
return Future.delayed(const Duration(seconds: 1), () => 'Hello, world!');
}
Future<void> main() async {
print('Fetching...');
String greeting = await fetchGreeting(); // pauses here until the Future resolves
print(greeting);
print('Done!');
}
// Fetching...
// Hello, world! <- ~1 second later
// Done!
Any function marked async automatically returns a Future (wrapping
whatever type you return), even if you never explicitly write Future<...>.
Error handling with try/catch¶
Future<int> riskyDivision(int a, int b) async {
await Future.delayed(const Duration(milliseconds: 100));
if (b == 0) throw ArgumentError('Cannot divide by zero');
return a ~/ b;
}
Future<void> main() async {
try {
int result = await riskyDivision(10, 2);
print('Result: $result'); // Result: 5
int bad = await riskyDivision(10, 0);
print('Unreachable: $bad');
} catch (e) {
print('Caught error: $e');
// Caught error: Invalid argument(s): Cannot divide by zero
}
}
Running several futures concurrently with Future.wait¶
Awaiting one Future at a time runs them sequentially. To run independent
async operations concurrently, use Future.wait.
Future<String> fetchUser() async {
await Future.delayed(const Duration(seconds: 1));
return 'Ada';
}
Future<int> fetchScore() async {
await Future.delayed(const Duration(seconds: 1));
return 95;
}
Future<void> main() async {
var stopwatch = Stopwatch()..start();
// Both futures run concurrently -- total wait is ~1s, not ~2s
var results = await Future.wait([fetchUser(), fetchScore()]);
print(results); // [Ada, 95]
print('Took ${stopwatch.elapsedMilliseconds}ms'); // ~1000ms, not ~2000ms
}
Timer for delayed or repeating work¶
import 'dart:async';
void main() {
print('Start');
Timer(const Duration(seconds: 1), () {
print('One second later');
});
int ticks = 0;
var repeater = Timer.periodic(const Duration(milliseconds: 500), (timer) {
ticks++;
print('Tick $ticks');
if (ticks == 3) timer.cancel();
});
}
A tiny intro to Stream¶
A Stream<T> is like a Future<T> that can produce many values over time
instead of just one — covered in depth in Level 2. Here's a first taste:
Stream<int> countUpTo(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(const Duration(milliseconds: 200));
yield i; // emit each value as it becomes ready
}
}
Future<void> main() async {
await for (var number in countUpTo(3)) {
print(number);
}
// 1
// 2
// 3
}
Cheat sheet¶
| Concept | Meaning |
|---|---|
Future<T> |
A value of type T that will be available later (or fail) |
async |
Marks a function as asynchronous; it returns a Future |
await |
Pauses inside an async function until a Future resolves |
.then(callback) |
Runs callback once the Future completes (non-await style) |
Future.wait([...]) |
Runs multiple futures concurrently, waits for all to finish |
try/catch |
Standard way to handle errors from an awaited Future |
Stream<T> |
Like a Future, but can emit many values over time |
How It Actually Works¶
Dart is single-threaded within an isolate — there is exactly one call
stack, and async/await never spawns a thread. What actually happens is
that each isolate runs an event loop built around two queues: the
microtask queue and the event queue. When you await a Future
that isn't already completed, the current async function's remaining code
is packaged up as a continuation and the function returns control to its
caller immediately — the isolate is free to run other code. When the
awaited operation completes (a timer fires, I/O finishes, someone calls
Completer.complete()), its continuation is scheduled: Future callbacks
(.then, the resumption of an await) go on the microtask queue, which
is always fully drained before the event loop looks at the event queue
(timers, I/O callbacks, Stream events). This priority is exactly why
chaining many .then()/await steps can starve timer callbacks if you're
not careful — each microtask can schedule another microtask, and they all
run before a single Timer gets a turn.
Future.wait doesn't run futures "in parallel" in the threading sense —
there's still one isolate, one stack. What it does is start all the given
asynchronous operations (which is only meaningful if those operations
themselves do concurrent work like real I/O, since I/O in Dart's dart:io
is handled by the underlying event notification system, not by Dart code
looping) before awaiting any of their results, so their waiting time
overlaps instead of being serialized — the CPU-bound Dart code for each
future still runs one snippet at a time, interleaved via the event loop, but
the wall-clock wait for e.g. three independent network calls collapses to
roughly the slowest one instead of the sum of all three.
A try/catch around an await works because the compiler transforms your
async function into a state machine under the hood — each await point is
a suspension point, and exceptions thrown inside the asynchronous operation
are captured and delivered back into that state machine's catch handling
exactly as if the code had executed synchronously, even though real
(unrelated) code from other parts of the event loop may have run on the same
thread in between.
🔀 See this in another language¶
Exercise¶
Write an async function fetchTemperature(String city) that simulates a
network call with Future.delayed (random 200-800ms) and returns a random
double temperature. Call it for three different cities concurrently with
Future.wait, print all three results, and print how long the whole
operation took using a Stopwatch — confirm it's close to the slowest
individual call, not the sum of all three.