Future and async/await
A Future<T> represents one value (or error) that will arrive later. It has three states: uncompleted → completed with value or completed with error. Once completed, the state never changes.
The core rules:
- An
asyncfunction always returns aFuture. EvenFuture<int> f() async => 42;gives you aFuture<int>, not anint. If it returns nothing, it isFuture<void>. awaitsuspends that function until the future completes — not the whole program; the event loop keeps running.- An exception thrown inside an
asyncfunction is not thrown synchronously: it completes the returned future with an error. So the callf()itself will not be caught by a surroundingtry—await f()will. - An un-awaited ("floating") future still runs; you simply never see its result — or its error. This is the single most common async bug.
| Construct | What it does | When to use it |
|---|---|---|
| `Future.value(x)` | A future already completed with a value | Fakes in tests, cache hits, adapting a sync result to an async API |
| `Future.error(e)` | A future already completed with an error | Testing the failure path |
| `Future.delayed(d, fn)` | Runs `fn` on the event queue after `d` | Retry backoff, simulation; **replace with `FakeAsync` in tests** |
| `Future.wait([...])` | Awaits all in parallel, returns a `List` | Independent calls |
| `Future.any([...])` | The result of the first one to complete | Timeout patterns, racing sources |
| `future.timeout(d)` | Throws `TimeoutException` after `d` | Apply it to every network call |
Sequential or parallel? This is the performance flaw that shows up most often in code review.
final a = await getUser(); final b = await getOrders();— with no dependency between them, this adds the durations up.final [a, b] = await Future.wait([getUser(), getOrders()]);— both start at once and the total is the slowest of the two.
Watch out: Future.wait is fail-fast — the combined future completes with the first error, but the others are not cancelled; they keep running in the background. If they fail too, those errors go unhandled. When you need each outcome separately, attach a per-future catchError or wrap results in a Result type.
`then`/`catchError` or `await`? The semantics are the same, but await wins: the stack trace stays readable, try/catch/finally works, and conditions and loops read naturally. then remains useful only when you do not want to make the function async (for instance you just transform a future and return it).
The async gap — the most important idea in this topic. Everywhere you write await, your function suspends and control returns to the event loop. In that gap anything else can happen: the user closes the screen, the object is disposed, the same function is called again, the cache is cleared.
Two practical rules follow:
- Re-check any state you read after an `await`. A condition that held before the await may not hold after it. In Flutter this is the
if (!mounted) return;rule, and the analyzer'suse_build_context_synchronouslywarning is about exactly this problem. - Control re-entrancy. If two
refresh()calls are in flight, there is no guarantee about which result gets written last. Either cancel the previous one, or validate the result against a request id.
The async gap is also the main source of timing-dependent bugs: they never show up on your machine, where the network is fast and the gap is short.
Interview tip. Three questions are near-guaranteed.
"What does an `async` function return?" — answer "always a Future", then immediately add the consequence: that is why an exception thrown inside it is not thrown synchronously but completes the future with an error.
"How would you speed up these two awaits?" — say Future.wait, then attach the condition: only if there is no dependency between them. A candidate who omits that condition gave half an answer.
"What is the async gap?" — explain that the world may have changed after an await and give a concrete example: a _disposed check (in Flutter, mounted) after the await. The most common mistake is saying await "pauses the program". Only that function pauses — the event loop keeps running.
📚 Sources and documentation
- Asynchronous programmingofficialdart.dev
- Future classofficialapi.dart.dev