Sparround

Future and async/await

A Future<T> represents one value (or error) that will arrive later. It has three states: uncompletedcompleted with value or completed with error. Once completed, the state never changes.

The core rules:

  • An async function always returns a Future. Even Future<int> f() async => 42; gives you a Future<int>, not an int. If it returns nothing, it is Future<void>.
  • await suspends that function until the future completes — not the whole program; the event loop keeps running.
  • An exception thrown inside an async function is not thrown synchronously: it completes the returned future with an error. So the call f() itself will not be caught by a surrounding tryawait 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.
ConstructWhat it doesWhen to use it
`Future.value(x)`A future already completed with a valueFakes in tests, cache hits, adapting a sync result to an async API
`Future.error(e)`A future already completed with an errorTesting 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 completeTimeout 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's use_build_context_synchronously warning 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