Skip to content

Async in Dart

Dart runs your code on a single thread with an event loop — the same model as JavaScript. There is no shared-memory threading in your normal code. Instead, async operations (network, files, timers) run elsewhere and schedule a callback on the event loop when they finish.

This is why “async” in Flutter does not mean “another thread.” It means “don’t block the one thread we have.” A slow synchronous loop freezes the UI; an await on a network call does not, because the thread is free to render frames while waiting.

flowchart LR
  ui["UI thread
(event loop)"] -->|await fetch| free["thread is free
to render frames"]
  free -->|response arrives| cb["callback scheduled
on the loop"]
  cb --> resume["your code resumes"]
Async work frees the single thread to keep rendering

A Future<T> is a value that will exist later — the result of an async operation. async/await lets you write it as if it were sequential.

Future<User> fetchUser(int id) async {
final response = await http.get(Uri.parse('/users/$id')); // waits, doesn't block
return User.fromJson(response.body);
}
// Calling it:
Future<void> load() async {
try {
final user = await fetchUser(42);
print(user.name);
} catch (e) {
print('failed: $e'); // awaited futures throw errors you can try/catch
}
}

An async function always returns a Future. await unwraps it, and — crucially — errors from an awaited future surface as normal exceptions you can try/catch. In Flutter you rarely await in build; instead you hand the Future to a FutureBuilder (a later module).

A Stream<T> is like a Future that delivers many values — a sequence of events over time: web-socket messages, sensor readings, user input, database changes.

Stream<int> ticker() async* { // async* makes a Stream; yield emits a value
for (var i = 1; i <= 3; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i; // emits 1, then 2, then 3
}
}
// Consume with await-for or .listen:
await for (final tick in ticker()) {
print(tick); // 1, 2, 3 — one per second
}

The Future/Stream split maps cleanly to Flutter: one-shot async → FutureBuilder; a stream of updates → StreamBuilder. Getting this distinction right is most of what “async in Flutter” means.

Isolates: true parallelism, when you need it

Section titled “Isolates: true parallelism, when you need it”

The one exception to “single thread” is isolates — separate workers with their own memory that communicate by message passing (no shared state). You reach for one only for genuinely CPU-heavy work (parsing a huge JSON, image processing) that would otherwise jank the UI. Flutter wraps the common case in compute(), covered in the performance module.

What does async mean in Dart's execution model?
What is the difference between a Future and a Stream?
How do errors from an awaited Future surface?
When should you reach for an isolate?