Async & Futures
Future, async/await, then/catchError, FutureBuilder
- 01
Explain Future, async/await, and error handling in Dart.
EasyA
Future<T>is a placeholder for a value that will arrive later, or for the error that arrives instead.// Basic Future Future<String> fetchUser(int id) async { await Future.delayed(const Duration(seconds: 1)); // simulate network if (id <= 0) throw ArgumentError('Invalid id'); return 'User #$id'; } … - 02
What is a Completer in Dart and when would you use it?
MediumA
Completer<T>creates a Future that you can complete manually — useful when you need to bridge callback-based APIs with Futures.// ── Basic usage ──────────────────────────────────── Future<String> waitForUser() { final completer = Completer<String>(); // Simulate callback-based API someCallbackAPI( … - 03
How do you run several async operations concurrently, and what happens when one of them fails?
MediumStart the futures first and await them afterwards — that is what makes them concurrent; awaiting each call in turn makes them sequential.
// ── Future.wait() — parallel execution ────── Future<void> loadAllData() async { try { final results = await Future.wait([ fetchUser(), fetchPosts(), … - 04
Explain Dart's event loop, microtask queue, and event queue.
MediumEach Dart isolate is single-threaded and runs an event loop that fully drains a microtask queue before it takes the next event.
// Order observation void main() { print('1'); Future(() => print('2 - event')); scheduleMicrotask(() => print('3 - microtask')); Future.microtask(() => print('4 - microtask')); … - 05
How do timeouts and retries work for Futures?
MediumRobust async code accepts that things take too long or fail transiently.
// ── Timeout with fallback ── Future<List<Hit>> loadResults(String q) async { return api.search(q).timeout( const Duration(seconds: 5), onTimeout: () => const <Hit>[], // graceful fallback ); … - 06
What is fire-and-forget and when is it safe to use unawaited()?
EasyA "fire-and-forget" Future is one whose result you don't await — analytics events, log writes, prefetches in the background.
import 'dart:async'; // unawaited() // ── Analytics — safe to fire-and-forget if errors are absorbed ── void recordView(String screen) { unawaited( analytics.track('view', {'screen': screen}) … - 07
How do .then(), .catchError(), and chaining compare to async/await?
MediumBoth styles build the same Future, so the choice is about readability and a handful of error-handling details.
// ── Same logic, both styles ── // .then chain Future<Profile> loadProfileChain(String id) { return api.user(id) .then((u) => api.profile(u.id)) … - 08
What is FutureBuilder and what are its common pitfalls?
MediumFutureBuilder<T> rebuilds itself based on the state of a Future.
// ── ❌ Anti-pattern — fetch on every rebuild ── class BadProfile extends StatelessWidget { const BadProfile({super.key, required this.id}); final String id; @override Widget build(BuildContext context) { … - 09
How do you serialize concurrent access to a shared resource in Dart?
HardEven though Dart is single-threaded per isolate, async functions interleave at every
await.import 'dart:async'; import 'package:synchronized/synchronized.dart'; // ── 1) Single-flight refresh ── class TokenManager { Future<Tokens>? _refreshing; … - 10
What is
FutureOr<T>and when would a function return it instead ofFuture<T>?MediumFutureOr<T> is the union of T and Future<T>, so a value that is already available can be returned without going through the event loop at all.
import 'dart:async'; FutureOr<User> getUser(String id) { final cached = _cache[id]; if (cached != null) return cached; // synchronous — no event-loop turn return _fetchAndCache(id); // asynchronous … - 11
Dart Futures cannot be cancelled. How do you stop work the user has already navigated away from?
MediumCancellation has to come from whatever started the work, because a Future is only a handle to a result and has no idea what produced it.
// ── Source-level: Dio CancelToken ── class SearchRepo { final _dio = Dio(); CancelToken? _token; Future<List<Hit>> search(String q) async { … - 12
Which errors does Flutter catch for you, and how do you make sure an uncaught async error still gets reported?
MediumFlutter catches whatever is thrown inside its own callbacks and turns it into the error screen, while an uncaught asynchronous error escapes to the platform dispatcher instead.
void main() { WidgetsFlutterBinding.ensureInitialized(); FlutterError.onError = (FlutterErrorDetails details) { FlutterError.presentError(details); // keep the console output crashReporter.recordFlutterError(details); … - 13
An async function runs partly at once and partly later — where exactly does that line fall?
EasyAn async function's body runs synchronously up to its first
await; only there does it suspend and hand the caller a future.Future<int> fetch() async { print('2 sync prefix — same turn as the call'); final res = await api.get('/count'); // suspends here; caller gets a Future print('5 continuation — a later turn'); return res.length; } … - 14
The spinner freezes while the app awaits a jsonDecode of a few megabytes — if it is awaited, why did the UI stop?
EasyBecause
awaitonly lets the event loop run other work while something else is pending — it never moves your own code off the isolate's single thread.// Wrong: async, awaited, and still 400 ms of dropped frames Future<List<Post>> loadPosts() async { final res = await http.get(uri); // real waiting — the loop is free here final json = jsonDecode(res.body); // pure CPU — the loop is blocked return (json as List) .map((e) => Post.fromJson(e as Map<String, dynamic>)) … - 15
Future.wait over 500 ids fires 500 requests at once and the API starts answering 429 — how do you fix that without going sequential?
MediumCap how many futures are in flight at once: run a small pool of workers over a shared queue instead of starting the whole list.
// Floods: 500 requests start in the same event-loop turn final all = await Future.wait(ids.map(fetchOne)); // Bounded: N workers share one cursor over the list Future<List<R>> mapPooled<T, R>( Iterable<T> items, … - 16
You need a user and their settings in parallel, and Future.wait hands you a List of Object? — what does Dart 3 offer instead?
MediumThe record extension
.wait:(fetchUser(), fetchSettings()).waitcompletes with a typed(User, Settings)record, so nothing has to be downcast.// Dart 3: typed, destructured, all three in flight at once final (user, settings, flags) = await ( api.fetchUser(id), api.fetchSettings(id), api.fetchFlags(), ).wait; … - 17
The analyzer flags use_build_context_synchronously after an await in a button handler — what actually breaks if you ignore it?
MediumBy the time the await returns, the widget may be gone, and the
BuildContextyou captured before the gap now points at a defunct Element that can no longer be used to look anything up.// Wrong: context used after the gap Future<void> submit(BuildContext context) async { await api.save(form); // the user can pop during this Navigator.of(context).pop(); // may be a defunct Element ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text('Saved'))); … - 18
A repository method returns a Future but is not marked async, and its argument check throws — why does the caller's catchError never run?
MediumBecause the throw happens while the call is still on the caller's stack: there is no future yet for
catchErrorto attach to, so the exception propagates out of the call expression instead of into the chain.// Wrong: a Future-returning function that throws on the caller's stack Future<User> load(String id) { if (id.isEmpty) throw ArgumentError.value(id, 'id'); // synchronous return _dio.get('/users/$id').then(User.fromResponse); } … - 19
Crash reports show a stack trace that ends at the event loop instead of the failing line — what loses the origin of an async error?
HardThe report is only as good as the
(error, stackTrace)pair you carry: the VM stitches the async frames for you, but every place that re-throws or re-wraps an error without its originalStackTraceresets the origin to that line.// Loses the origin: a new throw builds a new trace try { await repo.save(order); } catch (e) { throw SaveFailed('$e'); // trace now starts on this line } … - 20
You store a Future in a field and await it in a try/catch a second later, yet the app still logs an unhandled exception — why?
HardBecause a future decides an error is unhandled at the moment it completes: if nothing is listening then, the error goes to the zone, and your later
awaitattaches a handler to a future that has already reported itself.// Minimal repro: the report goes out one turn after completion final f = Future<int>.error(StateError('boom')); await Future<void>.delayed(Duration.zero); try { await f; // your catch runs — and the console already has it } catch (_) {} …