Async & Futures
Irbisa · cheatsheetSeptember 13, 2026

Async & Futures

Future, async/await, then/catchError, FutureBuilder

Junior Developer20 itemscompressed for a skim
  1. 01

    Explain Future, async/await, and error handling in Dart.

    Easy

    A 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';
    }
    …
  2. 02

    What is a Completer in Dart and when would you use it?

    Medium

    A 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(
    …
  3. 03

    How do you run several async operations concurrently, and what happens when one of them fails?

    Medium

    Start 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(),
    …
  4. 04

    Explain Dart's event loop, microtask queue, and event queue.

    Medium

    Each 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'));
    …
  5. 05

    How do timeouts and retries work for Futures?

    Medium

    Robust 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
      );
    …
  6. 06

    What is fire-and-forget and when is it safe to use unawaited()?

    Easy

    A "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})
    …
  7. 07

    How do .then(), .catchError(), and chaining compare to async/await?

    Medium

    Both 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))
    …
  8. 08

    What is FutureBuilder and what are its common pitfalls?

    Medium

    FutureBuilder<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) {
    …
  9. 09

    How do you serialize concurrent access to a shared resource in Dart?

    Hard

    Even 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. 10

    What is FutureOr<T> and when would a function return it instead of Future<T>?

    Medium

    FutureOr<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. 11

    Dart Futures cannot be cancelled. How do you stop work the user has already navigated away from?

    Medium

    Cancellation 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. 12

    Which errors does Flutter catch for you, and how do you make sure an uncaught async error still gets reported?

    Medium

    Flutter 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. 13

    An async function runs partly at once and partly later — where exactly does that line fall?

    Easy

    An 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. 14

    The spinner freezes while the app awaits a jsonDecode of a few megabytes — if it is awaited, why did the UI stop?

    Easy

    Because await only 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. 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?

    Medium

    Cap 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. 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?

    Medium

    The record extension .wait: (fetchUser(), fetchSettings()).wait completes 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. 17

    The analyzer flags use_build_context_synchronously after an await in a button handler — what actually breaks if you ignore it?

    Medium

    By the time the await returns, the widget may be gone, and the BuildContext you 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. 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?

    Medium

    Because the throw happens while the call is still on the caller's stack: there is no future yet for catchError to 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. 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?

    Hard

    The 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 original StackTrace resets 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. 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?

    Hard

    Because 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 await attaches 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 (_) {}
    …