Futures, async/await & the Event Loop
The #1 Most-Asked Dart Interview Topic
Open interactive version (quiz + challenge)Real-world analogy
Imagine ordering food at a restaurant. You place the order (create a Future), then chat with friends (event loop continues). When the food arrives (Future completes), you eat it (then/await). You don't stand at the kitchen door blocking everyone. That's async programming — do other work while waiting.
What is it?
Futures and async/await are Dart's core async primitives. A Future represents a value that will arrive later. async/await makes async code readable. The event loop processes tasks in a single thread using microtask and event queues. Understanding this system is tested in 80%+ of Flutter interviews.
Real-world relevance
In a claims processing app, when a user submits a refund request, the app: 1) Shows a loading spinner (Future starts), 2) Sends the API request (await), 3) Updates the UI with the result (Future completes). Meanwhile, the user can still scroll and interact because the event loop keeps the UI responsive.
Key points
- What is a Future? — A Future represents a value that will be available later. It's a promise: 'I'll give you a String eventually.' A Future is either uncompleted, completed with a value, or completed with an error. Every HTTP call, file read, and DB query returns a Future.
- async/await Syntax — Mark a function async to use await inside it. await pauses that function (not the whole app) until the Future completes. The function returns a Future automatically. Interview: async functions always return Future even if you return T directly.
- The Event Loop — Single Threaded — Dart is single-threaded with an event loop. Two queues: microtask queue (high priority) and event queue (normal). Microtasks run first. Future(() => ...) adds to event queue. Future.microtask() adds to microtask queue. This is the #1 async interview question.
- Event Loop Execution Order — Order: 1) Synchronous code runs first. 2) All microtasks drain. 3) One event processes. 4) Check microtasks again. Interview classic: print order of sync code, Future.microtask, Future, Future.delayed — know this cold.
- Future.then() vs await — .then() chains callbacks. await is syntactic sugar that makes async code look synchronous. They do the same thing differently. await is cleaner for sequential operations. .then() is useful for fire-and-forget or parallel chains.
- Error Handling in Async — Use try/catch with await. Use .catchError() with .then(). Unhandled Future errors crash in debug, are swallowed in release (dangerous!). Always handle errors. Interview: What happens to an unhandled Future error?
- Future.wait and Future.any — Future.wait([f1, f2, f3]) waits for ALL to complete (parallel execution). Future.any([f1, f2, f3]) returns as soon as ANY completes. Use wait for parallel API calls. Interview: How do you make multiple API calls simultaneously?
- Completer — Manual Future Control — Completer lets you create a Future and complete it manually later. Useful for wrapping callback-based APIs into Future-based APIs. You create a completer, return completer.future, and call completer.complete(value) when ready.
- Common Async Pitfalls — Forgetting await (fire-and-forget bug). Using await in a loop instead of Future.wait (sequential vs parallel). Not handling errors. Blocking the event loop with heavy sync work. These are interview trap questions.
- FutureBuilder in Flutter — FutureBuilder widget listens to a Future and rebuilds the UI when it completes. Has connectionState: waiting, active, done. Interview: Why should you NOT create a Future inside the build method? Answer: it creates a new Future every rebuild.
Code example
// Futures & Event Loop — Interview Must-Know
// Basic Future
Future<String> fetchUser() async {
// Simulates API call — pauses this function, not the app
await Future.delayed(Duration(seconds: 2));
return 'John Doe';
}
// Using async/await
Future<void> loadData() async {
try {
final user = await fetchUser();
print('Got: $user');
} catch (e) {
print('Error: $e');
}
}
// Using .then() chain
fetchUser()
.then((user) => print('Got: $user'))
.catchError((e) => print('Error: $e'));
// Parallel execution with Future.wait
Future<void> loadDashboard() async {
final results = await Future.wait([
fetchUser(),
fetchSettings(),
fetchNotifications(),
]);
// All three complete in parallel, not sequentially!
final user = results[0];
final settings = results[1];
final notifications = results[2];
}
// EVENT LOOP ORDER — Interview Classic
void main() {
print('1 - Sync'); // Runs 1st
Future.microtask(() => print('2 - Microtask')); // Runs 3rd
Future(() => print('3 - Event queue')); // Runs 4th
Future.delayed(
Duration.zero,
() => print('4 - Delayed zero'), // Runs 5th
);
print('5 - Sync again'); // Runs 2nd
}
// Output: 1, 5, 2, 3, 4
// Completer — manual Future control
import 'dart:async';
Future<String> wrapCallback() {
final completer = Completer<String>();
someCallbackApi(
onSuccess: (data) => completer.complete(data),
onError: (e) => completer.completeError(e),
);
return completer.future;
}
// WRONG: await in a loop (sequential!)
for (var id in ids) {
await fetchItem(id); // Each waits for previous — slow!
}
// RIGHT: parallel execution
final items = await Future.wait(ids.map(fetchItem));Line-by-line walkthrough
- 1. Declaring an async function that returns Future
- 2. await pauses this function for 2 seconds, simulating an API call
- 3. Returns the string — automatically wrapped in a completed Future
- 4. The calling function is also async
- 5. try/catch handles both sync and async errors with await
- 6. awaiting the result — this line pauses until fetchUser completes
- 7. catch handles any error that fetchUser might throw
- 8. Alternative .then() syntax — same behavior, different style
- 9. catchError handles errors in the .then() chain
- 10. Future.wait takes a list of Futures and runs them ALL in parallel
- 11. All three API calls happen simultaneously, not one after another
- 12. Destructuring the results list
- 13. The classic event loop question — sync code runs first
- 14. Microtask queue drains before the event queue
- 15. Event queue processes after all microtasks
Spot the bug
Future<void> loadData() async {
final data = fetchData();
print(data.length);
}
Future<List<int>> fetchData() async {
await Future.delayed(Duration(seconds: 1));
return [1, 2, 3];
}Need a hint?
What type does fetchData() return if you don't await it?
Show answer
Missing await! 'final data = fetchData()' assigns a Future<List<int>>, not a List<int>. Calling .length on a Future is a compile error (or if dynamic, a runtime crash). Fix: 'final data = await fetchData();'
Explain like I'm 5
Imagine you're at a pizza shop. You order a pizza (that's a Future — a promise of pizza). While it bakes, you go sit down and play on your phone (the event loop doing other work). When the pizza is ready, the counter calls your name (the Future completes). You go grab it (await gets the value). You didn't just stand at the counter staring — you did other stuff while waiting!
Fun fact
Dart's event loop is inspired by JavaScript's event loop, but Dart added a separate microtask queue that drains completely before the next event. This is why Future.microtask() always runs before Future() — a common interview trick question.
Hands-on challenge
Write a function fetchAllUsers() that takes a list of user IDs and fetches all user profiles in parallel using Future.wait. Include proper error handling with try/catch. If any single fetch fails, return the successfully fetched users and log the failures.
More resources
- Asynchronous Programming: Futures (Dart Official)
- Dart Event Loop (Dart Official)
- Futures and Error Handling (Dart Official)
- Completer API (Dart Official)