← Back to blog
Async & Concurrency in Dart · Part 10 of 10
July 14, 202631 min read

100 Questions to Master Async & Concurrency in Dart

DartAsyncFlutter

100 Questions to Master Async & Concurrency in Dart

This is Part 10 — the finale of the Async & Concurrency in Dart series. The previous nine parts taught the concepts; this is where you prove you own them.

How to use this bank:

  • 100 questions, grouped by topic, tagged [Basic], [Medium], [Advanced] and marked (Theory) or (Coding).
  • Each has a Hint (the idea / where to look) and a separate Solution (the actual answer). Try cold first, peek at the hint if stuck, and only then check the solution. The struggle is the learning.
  • For (Coding) questions, run your code — dartpad.dev needs zero setup. Predict the output before running; that's where async understanding is forged.
  • After the 100, there are 10 coding mini-exercises with full solutions, ending in a capstone.

If you can explain the why on all 100 without hints, you genuinely understand async Dart. Let's go.


Section A — The event loop & how Dart runs (Q1–12)

Q1. [Basic] (Theory) Is Dart single- or multi-threaded by default, and how does it appear to do many things at once?

Hint

One thread, one loop. See Part 1.

Solution

Single-threaded by default (one isolate). It appears concurrent via the event loop, which interleaves deferred callbacks — not by using multiple threads. True parallelism requires isolates.

Q2. [Basic] (Theory) Name Dart's two queues and which one the event loop drains first.

Hint

One is high-priority and emptied completely.

Solution

The microtask queue and the event queue. The loop empties the entire microtask queue before processing even one event-queue item.

Q3. [Basic] (Coding) Predict the output:

void main() {
  print('1');
  Future(() => print('2'));
  print('3');
}
Hint

Sync code first; the Future callback is deferred.

Solution

1, 3, 2. 1 and 3 are synchronous; the Future callback runs only after main finishes.

Q4. [Basic] (Theory) How do you add work to (a) the event queue and (b) the microtask queue?

Hint

One uses a constructor, one a function.

Solution

(a) Future(() {...}) / Future.delayed / timers / I/O add to the event queue. (b) scheduleMicrotask(() {...}) adds to the microtask queue (as does the continuation after an await).

Q5. [Medium] (Coding) Predict the output:

import 'dart:async';
void main() {
  print('A');
  Future(() => print('B'));
  scheduleMicrotask(() => print('C'));
  print('D');
}
Hint

Sync, then all microtasks, then events.

Solution

A, D, C, B. Synchronous (A,D) first; microtask (C) before event (B).

Q6. [Medium] (Theory) "An async function runs synchronously until ___." Complete and explain why it matters.

Hint

Think about the first suspension point.

Solution

"...until its first await." Code before the first await (validation, logging, kicking off requests) executes immediately in the current turn; only code after an await is deferred. So an async function is not fully "background".

Q7. [Medium] (Coding) Predict the output:

Future<void> f() async {
  print('B');
  await null;
  print('C');
}
void main() {
  print('A');
  f();
  print('D');
}
Hint

f() runs to its first await, then defers the rest.

Solution

A, B, D, C. f() prints B synchronously up to await null, which defers print('C'); control returns and prints D, then the continuation prints C.

Q8. [Medium] (Theory) Why does a long synchronous for loop freeze a Flutter UI?

Hint

What event can't run while the loop runs?

Solution

Drawing each frame is itself an event. A long synchronous loop monopolizes the single thread, so the "paint frame" event never gets its turn — the UI can't update until the loop finishes.

Q9. [Advanced] (Coding) Predict the output and explain:

import 'dart:async';
void main() {
  print('start');
  Future(() => print('e1'));
  Future(() { print('e2'); scheduleMicrotask(() => print('m')); });
  Future(() => print('e3'));
  scheduleMicrotask(() => print('m0'));
  print('end');
}
Hint

Microtasks drain after every event too.

Solution

start, end, m0, e1, e2, m, e3. Sync first; then all microtasks (m0); then events one at a time, draining microtasks after each — so m (scheduled inside e2) runs before e3.

Q10. [Advanced] (Theory) How can flooding the microtask queue harm an app?

Hint

Microtasks outrank events, including frame draws.

Solution

Because microtasks always run before the next event (and frames are events), an endless stream of microtasks starves the event queue — frames never paint and the UI freezes. Prefer Future(...) for normal deferred work.

Q11. [Advanced] (Theory) Does async/await give you parallelism? Justify.

Hint

What is executing while you await?

Solution

No. It's concurrency on one thread — the thread interleaves tasks by parking work during I/O waits, but only one statement executes at any instant. It helps only when waiting, not computing. Parallelism needs isolates.

Q12. [Advanced] (Coding) Without changing semantics, why is scheduleMicrotask rarely the right call in app code, and what's the safer default?

Hint

Think starvation and intent.

Solution

It cuts ahead of all events (including frames) and can starve the loop; you almost never need something to run before the next event. The safer default for "do this later" is Future(() {...}), which uses the event queue and lets frames/I-O interleave.


Section B — Futures (Q13–25)

Q13. [Basic] (Theory) In one sentence, what is a Future<T>?

Hint

A buzzer, not the food. Part 2.

Solution

An object representing a value of type T that isn't available yet but will be produced — or will fail — at some later point.

Q14. [Basic] (Theory) What are a future's states/outcomes?

Hint

Two states; completion has two flavors.

Solution

Uncompleted or completed; completion is either with a value (success) or with an error (failure). A future completes exactly once.

Q15. [Basic] (Coding) Write Future<int> answer() that completes with 42 after one second; print it with .then.

Hint

Future.delayed.

Solution
Future<int> answer() => Future.delayed(const Duration(seconds: 1), () => 42);
void main() => answer().then(print); // 42

Q16. [Basic] (Coding) Fix the error:

Future<int> getCount() => Future.value(5);
void main() {
  int count = getCount();
  print(count + 1);
}
Hint

getCount() is a future, not an int.

Solution

Await it inside an async main: Future<void> main() async { final count = await getCount(); print(count + 1); }. You can't do arithmetic on a Future.

Q17. [Medium] (Theory) Map .then/.catchError/.whenComplete onto try/catch/finally.

Hint

Success / error / always.

Solution

.then = the success path; .catchError = catch; .whenComplete = finally (runs on success or error).

Q18. [Medium] (Coding) If a .then callback returns a Future<String>, what's the chain's type?

Hint

Dart flattens.

Solution

Future<String>, not Future<Future<String>>. Dart flattens returned futures — the chain waits for the inner future and adopts its value.

Q19. [Medium] (Coding) Two independent calls take 2s each. Get both results in ~2s.

Hint

Future.wait.

Solution
final results = await Future.wait([a(), b()]); // ~2s, both overlap

Sequential await a(); await b(); would take ~4s.

Q20. [Medium] (Theory) What does Future.wait do if one of its futures errors?

Hint

All-or-nothing.

Solution

The returned future completes with an error as soon as any input future errors. (Use eagerError/individual catchError, or Future.any for "first to finish", depending on need.)

Q21. [Medium] (Coding) Bridge a callback API loadConfig(onDone) into a Future<String>.

Hint

Completer.

Solution
Future<String> loadConfigFuture() {
  final c = Completer<String>();
  loadConfig((data) => c.complete(data));
  return c.future;
}

Q22. [Advanced] (Theory) When is a Completer the right tool over async/await?

Hint

When completion isn't already a future.

Solution

When completion is driven by something that isn't a future — a callback API, a one-shot event/listener, a button tap, a native callback. You hand out completer.future now and fire complete/completeError later (exactly once).

Q23. [Advanced] (Coding) Predict the output:

void main() {
  Future.value(1).then((v) => print('a $v'));
  print('b');
}
Hint

Even an already-completed future schedules its .then.

Solution

b, then a 1. .then callbacks are always scheduled (as microtasks), so synchronous b prints first even though the future is already complete.

Q24. [Advanced] (Theory) What's the difference between Future.value(x) and Future(() => x) regarding when the callback/value is delivered?

Hint

Microtask vs event queue.

Solution

Future.value(x) completes via the microtask queue (its .then runs sooner). Future(() => x) schedules the computation on the event queue (runs after pending microtasks and possibly other events). Different timing, same eventual value.

Q25. [Advanced] (Coding) Add a 2-second timeout to a future, throwing if it's too slow.

Hint

.timeout(...).

Solution
final r = await slow().timeout(const Duration(seconds: 2));
// throws TimeoutException if slow() takes longer; add onTimeout: for a fallback

Section C — async / await (Q26–40)

Q26. [Basic] (Theory) What does marking a function async change about its return type?

Hint

It always returns one thing.

Solution

It always returns a Future. Even return 42; becomes a Future<int> to the caller. (Part 3)

Q27. [Basic] (Coding) Rewrite using async/await:

Future<String> load() =>
  fetchToken().then((t) => fetchUser(t)).then((u) => u.name);
Hint

Each .then → an await.

Solution
Future<String> load() async {
  final t = await fetchToken();
  final u = await fetchUser(t);
  return u.name;
}

Q28. [Basic] (Theory) Can you use await outside an async function?

Hint

No — with one modern exception.

Solution

No; await requires an async function body. (Top-level main can be async. Some setups also allow top-level await in scripts, but inside a function you must mark it async.)

Q29. [Medium] (Coding) This loop over 10 independent fetches (1s each) is slow. Speed it up.

for (final id in ids) results.add(await fetch(id));
Hint

Independent → overlap.

Solution

final results = await Future.wait(ids.map(fetch)); — ~1s instead of ~10s. Keep the sequential loop only if each fetch depends on the previous result.

Q30. [Medium] (Theory) Why might this not catch the error?

try { riskyAsync(); } catch (e) { ... }
Hint

No await, no catch.

Solution

riskyAsync() isn't awaited, so control leaves the try before it fails; the error becomes unhandled. Add await riskyAsync(); inside the try.

Q31. [Medium] (Coding) Fix the "setState after dispose" crash:

Future<void> refresh() async {
  final d = await api.fetch();
  setState(() => _data = d);
}
Hint

The widget may be gone after the await.

Solution
final d = await api.fetch();
if (!mounted) return;
setState(() => _data = d);

Q32. [Medium] (Theory) Why can't build() be async?

Hint

Synchronous contract.

Solution

build() must return a Widget now for the current frame; an async function returns a Future<Widget> the framework can't paint. Use FutureBuilder/StreamBuilder and create the future once (in initState/state mgmt).

Q33. [Medium] (Coding) What's wrong with myList.forEach((e) async { await save(e); });?

Hint

forEach doesn't await.

Solution

forEach ignores the returned futures, so it fires them all without waiting and you can't catch their errors. Use a real for loop: for (final e in myList) { await save(e); } (or Future.wait(myList.map(save)) if independent).

Q34. [Medium] (Coding) Predict the output:

Future<void> main() async {
  print('1');
  await Future(() => print('2'));
  print('3');
}
Hint

await suspends until the future completes.

Solution

1, 2, 3. The await suspends main until the event-queue future runs (2), then resumes (3).

Q35. [Advanced] (Theory) What is unawaited(...) for?

Hint

Deliberate fire-and-forget.

Solution

It marks a future as intentionally not awaited (silencing the unawaited_futures lint) so readers know it's deliberate — e.g. unawaited(analytics.log(...)). You still forgo error handling on it.

Q36. [Advanced] (Coding) A dependent step then two independent steps with cleanup — write the idiomatic shape.

Hint

await the dependent; Future.wait the independents; finally.

Solution
try {
  final user = await fetchUser(id);            // dependent
  final r = await Future.wait([posts(user), friends(user)]); // independent
  return Dashboard(user, r[0], r[1]);
} finally { hideLoading(); }

Q37. [Advanced] (Coding) Predict the order:

Future<void> main() async {
  print('A');
  Future<void> t() async { print('B'); await Future.delayed(Duration.zero); print('C'); }
  t();
  print('D');
}
Hint

Runs sync to first await.

Solution

A, B, D, C. t() prints B synchronously, defers C at the await, control returns and prints D, then C.

Q38. [Advanced] (Theory) Why is over-using async (on non-async functions) a (small) cost?

Hint

Each boundary hops the loop.

Solution

Every async/await boundary adds a Future wrapper and an event-loop hop (a microtask). For purely synchronous logic that's needless overhead — keep such functions synchronous.

Q39. [Advanced] (Coding) Run three tasks but proceed as soon as the first completes.

Hint

Future.any.

Solution

final first = await Future.any([a(), b(), c()]); — completes with the first to finish (value or error).

Q40. [Advanced] (Theory) Lints to enable for async safety, and what each catches?

Hint

Two related rules.

Solution

unawaited_futures (a future isn't awaited where one is expected) and discarded_futures (a future-returning call is used as if sync). Together they catch most "forgot await" bugs.


Section D — Streams: basics (Q41–53)

Q41. [Basic] (Theory) Core difference between a Future and a Stream?

Hint

One value vs many over time. Part 4.

Solution

A Future delivers one value (or error) once; a Stream delivers a sequence of zero-to-many values over time, optionally with errors, ending in a "done" event.

Q42. [Basic] (Theory) Name a stream's three kinds of events.

Hint

data / ? / ?

Solution

Data events, error events, and a single done event.

Q43. [Basic] (Coding) Sum a Stream<int> with await for.

Hint

Loop, then return after done.

Solution
Future<int> total(Stream<int> s) async {
  var sum = 0;
  await for (final v in s) sum += v;
  return sum;
}

Q44. [Basic] (Theory) Two ways to consume a stream, and when to use each?

Hint

Loop vs callbacks.

Solution

await for (loop-like; continue after done) when you process every event then move on; .listen() (onData/onError/onDone + a StreamSubscription) when you don't want to block and need pause/resume/cancel.

Q45. [Medium] (Coding) Stop an infinite Stream.periodic after 3 events.

Hint

Cancel the subscription.

Solution
var n = 0; late StreamSubscription<int> sub;
sub = Stream.periodic(d, (i) => i).listen((v) { if (++n == 3) sub.cancel(); });

Q46. [Medium] (Theory) Single-subscription vs broadcast — the key differences?

Hint

Listener count + replay.

Solution

Single-subscription: exactly one listener, buffers/waits so the listener sees the full ordered sequence. Broadcast: many listeners, fires regardless of listeners, latecomers miss earlier events.

Q47. [Medium] (Coding) Why does the second .listen throw, and fix it?

final s = Stream.fromIterable([1,2,3]);
s.listen(print); s.listen(print);
Hint

Default stream type.

Solution

fromIterable is single-subscription — only one .listen. Use .asBroadcastStream() to allow multiple listeners (latecomers may miss events).

Q48. [Medium] (Coding) From integers 1..10, print squares of odds, first three only.

Hint

where → map → take.

Solution
Stream.fromIterable(List.generate(10, (i) => i + 1))
  .where((n) => n.isOdd).map((n) => n * n).take(3).listen(print); // 1,9,25

Q49. [Medium] (Theory) Why must you cancel a StreamSubscription, and where in Flutter?

Hint

Leaks + dispose.

Solution

A live subscription keeps the stream and callback alive (memory leak; can cause "setState after dispose"). Cancel it in the State's dispose(): _sub?.cancel().

Q50. [Advanced] (Theory) What does asyncMap do that map can't?

Hint

Returns a future per event.

Solution

asyncMap's transform returns a Future that's awaited per event (in order), enabling async work per item. map only supports synchronous transforms.

Q51. [Advanced] (Coding) Convert a stream into a single Future<List<T>>.

Hint

Terminal reducer.

Solution

final list = await stream.toList(); — a terminal operation that consumes the whole stream and returns a Future.

Q52. [Advanced] (Theory) A late broadcast subscriber misses earlier events. Why, and how do single-subscription streams differ?

Hint

Buffering for future listeners.

Solution

Broadcast streams don't buffer for future listeners — they fire regardless of who's listening, so latecomers see only subsequent events. Single-subscription streams wait for their one listener and deliver the full sequence from the start.

Q53. [Advanced] (Coding) Sketch a leak-free Flutter State listening to a stream.

Hint

initState listen, dispose cancel, mounted guard.

Solution
StreamSubscription<int>? _sub;
@override void initState() { super.initState();
  _sub = ticker.listen((v) { if (!mounted) return; setState(() => _v = v); }); }
@override void dispose() { _sub?.cancel(); super.dispose(); }

Section E — StreamController (Q54–63)

Q54. [Basic] (Theory) The two ends of a StreamController?

Hint

Read vs write. Part 5.

Solution

controller.stream (read end, for listeners) and controller.sink / add/addError/close (write end, to push events).

Q55. [Basic] (Coding) Create a controller, listen, push "a","b", then finish.

Hint

Don't forget close.

Solution
final c = StreamController<String>();
c.stream.listen(print, onDone: () => print('done'));
c.add('a'); c.add('b'); c.close();

Q56. [Basic] (Theory) Which method emits "done", and why must you call it?

Hint

Close & release.

Solution

close(). It signals completion (ends await for, fires onDone, tells StreamBuilder it's over) and releases the controller. Not calling it leaks.

Q57. [Medium] (Theory) Purpose of onListen and onCancel?

Hint

Tie work to demand.

Solution

onListen fires when the first listener subscribes — start producing then. onCancel fires when the last leaves — clean up (timers, sockets). Don't produce before anyone listens.

Q58. [Medium] (Coding) Emit an int every 500ms only while listened; stop cleanly on cancel.

Hint

Timer in onListen, cancel in onCancel.

Solution
late StreamController<int> c; Timer? t; var n = 0;
c = StreamController<int>(
  onListen: () => t = Timer.periodic(const Duration(milliseconds: 500), (_) => c.add(n++)),
  onCancel: () => t?.cancel());

Q59. [Medium] (Theory) Why pass controller.sink instead of the whole controller to a producer?

Hint

Encapsulation.

Solution

The sink exposes only add/addError/close — producers can push but can't listen or tear down the controller. It enforces clean read/write separation (basis of event buses, BLoC).

Q60. [Medium] (Coding) Make a broadcast controller with two listeners receiving the same event.

Hint

.broadcast().

Solution
final c = StreamController<int>.broadcast();
c.stream.listen((v) => print('one $v'));
c.stream.listen((v) => print('two $v'));
c.add(9); // both get 9

Q61. [Advanced] (Theory) What is backpressure and how does a controller help?

Hint

Producer faster than consumer.

Solution

Backpressure = producing faster than the consumer can handle; undelivered events buffer (and can grow unbounded). onPause/onResume let a well-behaved producer stop generating when the consumer pauses and resume when it catches up.

Q62. [Advanced] (Theory) Why avoid StreamController(sync: true)?

Hint

Immediate re-entrant delivery.

Solution

A synchronous controller delivers events immediately inside the add() call, re-entering listener code at surprising times and breaking stream guarantees. Use the default (async) controller unless you have a specific, understood reason.

Q63. [Advanced] (Coding) Wrap an encapsulated, type-safe broadcast event bus (emit, events, dispose).

Hint

Hide the controller.

Solution
class EventBus<T> {
  final _c = StreamController<T>.broadcast();
  Stream<T> get events => _c.stream;
  void emit(T e) => _c.add(e);
  Future<void> dispose() => _c.close();
}

Section F — Generators: sync* / async* (Q64–73)

Q64. [Basic] (Theory) What does sync* return vs async*?

Hint

Iterable vs Stream. Part 6.

Solution

sync* returns an Iterable<T>; async* returns a Stream<T>. Both use yield.

Q65. [Basic] (Coding) Write a sync* range(start, end) yielding start..end-1.

Hint

Loop + yield.

Solution
Iterable<int> range(int s, int e) sync* { for (var i = s; i < e; i++) yield i; }

Q66. [Basic] (Theory) What does yield do?

Hint

Emit + pause.

Solution

Emits one value and pauses the generator there; it resumes from that point when the consumer pulls the next value.

Q67. [Medium] (Coding) Prove laziness: an infinite sync* of evens, take 4.

Hint

while(true) yield.

Solution
Iterable<int> evens() sync* { var n = 0; while (true) yield n += 2; }
print(evens().take(4).toList()); // [2,4,6,8] — never hangs

Q68. [Medium] (Theory) Difference between yield and yield*?

Hint

One vs all.

Solution

yield emits one value; yield* delegates to another iterable/stream, emitting all of its values in place (great for recursion; also more efficient than manual re-yielding).

Q69. [Medium] (Coding) Flatten [[1,2],[3],[4,5]] with a generator.

Hint

yield* each inner.

Solution
Iterable<int> flatten(List<List<int>> n) sync* { for (final i in n) yield* i; }
// 1,2,3,4,5

Q70. [Medium] (Coding) async* stream yielding "tick" every 300ms, five times.

Hint

Await inside the loop.

Solution
Stream<String> ticks() async* {
  for (var i = 0; i < 5; i++) { await Future.delayed(const Duration(milliseconds: 300)); yield 'tick'; }
}

Auto-closes when the loop ends.

Q71. [Advanced] (Theory) Why can a lazy sync* represent an infinite sequence safely?

Hint

Only computes what's pulled.

Solution

Generators run only far enough to produce requested values, pausing at each yield. take(n) pulls exactly n, so the infinite body never runs to completion. (A List would try to build everything and hang.)

Q72. [Advanced] (Theory) What happens to an async* body when its listener cancels?

Hint

Next yield acts like return.

Solution

The next time the body reaches a yield, it behaves like a return — the generator stops and the stream ends cleanly. The machinery handles cancellation for you.

Q73. [Advanced] (Coding) Pick the tool: (a) Fibonacci on demand; (b) socket messages to many listeners; (c) paginated DB rows needing await per page.

Hint

sync* / controller / async*.

Solution

(a) sync* (pure lazy, no await); (b) StreamController.broadcast (external push, many listeners); (c) async* (your loop, awaits per page).


Section G — Error handling (Q74–85)

Q74. [Basic] (Theory) Complete: "try/catch catches an async error only if ___."

Hint

The golden rule. Part 7.

Solution

"...you await the failing future inside the try." No await, no catch.

Q75. [Basic] (Coding) Fix so the error is caught:

try { fetch(); } catch (e) { print('handled'); }
Hint

Add one keyword.

Solution

try { await fetch(); } catch (e) { print('handled'); }.

Q76. [Basic] (Theory) Which Future callbacks correspond to catch and finally?

Hint

Two methods.

Solution

.catchError() = catch; .whenComplete() = finally.

Q77. [Medium] (Coding) Handle TimeoutException and FormatException differently with a catch-all.

Hint

on Type catch.

Solution
try { await work(); }
on TimeoutException { ... }
on FormatException catch (e) { ... }
catch (e, s) { log(e, s); rethrow; }

Q78. [Medium] (Theory) rethrow vs throw e — why prefer rethrow?

Hint

Stack trace.

Solution

rethrow re-throws the same error with its original stack trace; throw e resets the trace to the current line, hiding the true origin.

Q79. [Medium] (Coding) Keep a stream alive past a divide-by-zero; print valid results, skip errors.

Hint

handleError.

Solution
Stream.fromIterable([2,0,4]).map((n) => 100 ~/ n)
  .handleError((e) => print('skip $e')).listen((v) => print('ok $v'));

Q80. [Medium] (Theory) With await for, an error event occurs. What happens to the loop?

Hint

It's thrown out.

Solution

The error is thrown out of await for, terminating the loop (it lands in a surrounding try/catch). To keep consuming, use .listen(onError:) or .handleError().

Q81. [Advanced] (Theory) How does stream error handling differ fundamentally from future error handling?

Hint

Count + ending.

Solution

A future fails at most once and is settled. A stream can emit multiple error events and an error doesn't necessarily end it (cancelOnError). So you choose stop-on-error (await for / cancelOnError: true) vs continue (onError/handleError).

Q82. [Advanced] (Coding) Why isn't this caught, and what's the right fix/safety net?

void main() { try { Future.error('boom'); } catch (e) { print('x'); } }
Hint

Unhandled async error.

Solution

The future fails on a later turn, after the sync try exited — it's an unhandled async error. Locally: await/catchError it. Globally: wrap in runZonedGuarded (or set FlutterError.onError/PlatformDispatcher.instance.onError).

Q83. [Advanced] (Theory) What's cancelOnError on .listen, and its default?

Hint

Stop after first error?

Solution

It decides whether the subscription is cancelled after the first error event. Default is false — the stream keeps delivering events after an error.

Q84. [Advanced] (Coding) Translate a low-level error at a boundary into an app error.

Hint

catch + throw domain error.

Solution
try { return await http.get(url); }
on SocketException catch (e) { throw AppError('Network down: $e'); }

Boundaries are where you convert technical errors into meaningful ones.

Q85. [Advanced] (Theory) Where should cleanup (hide spinner, close file) go, and why does it run even on error?

Hint

finally / whenComplete.

Solution

In finally (or .whenComplete). It runs regardless of success, exception, or rethrow, guaranteeing resources are released and UI state is reset on every path.


Section H — Isolates (Q86–95)

Q86. [Basic] (Theory) What problem do isolates solve that async/await can't?

Hint

CPU vs I/O. Part 8.

Solution

True parallelism for CPU-bound work. async/await only helps while waiting on I/O; a heavy computation blocks the single thread, so it must run on another thread — an isolate.

Q87. [Basic] (Theory) How do isolates share data?

Hint

They don't.

Solution

They don't share memory — each has its own memory and event loop. They communicate only via copied messages over SendPort/ReceivePort. No shared state → no locks, no data races.

Q88. [Basic] (Coding) Offload a CPU sum off the main thread.

Hint

Isolate.run.

Solution
final r = await Isolate.run(() => sumTo(100000000));

Q89. [Medium] (Theory) Why doesn't wrapping a tight CPU loop in async stop a UI freeze?

Hint

No new thread.

Solution

async doesn't add a thread; it only interleaves while waiting. A CPU loop never waits and never yields, so the event loop (including frame drawing) stays blocked. Move it to an isolate.

Q90. [Medium] (Coding) Parse a large JSON body off the UI thread in Flutter.

Hint

compute with a top-level fn.

Solution
List<Item> _parse(String b) => (jsonDecode(b) as List).map(Item.fromJson).toList();
final items = await compute(_parse, response.body);

Q91. [Medium] (Theory) Restriction on the function passed to compute()?

Hint

Not a closure.

Solution

It must be top-level or static (single message arg). Closures capturing local state can't be sent to another isolate.

Q92. [Medium] (Theory) Isolate.run vs Isolate.spawn — when each?

Hint

One-shot vs long-lived.

Solution

Isolate.run for one-shot work (it hides port wiring and auto-shuts-down). Isolate.spawn + ports for long-lived workers handling many messages.

Q93. [Advanced] (Theory) Describe the two-way handshake between main and a spawned worker.

Hint

Exchange SendPorts.

Solution

Main creates a ReceivePort and passes its sendPort on spawn; the worker creates its own ReceivePort and sends its sendPort back as the first message. Now each holds the other's SendPort → two-way comms.

Q94. [Advanced] (Theory) Which can't be sent across isolates: int, List<String>, open Socket, Map<String,int>?

Hint

Native resources.

Solution

The open Socket can't be sent (native resource bound to the origin isolate). The others are sendable. (Also non-sendable: ReceivePort, Pointer, DynamicLibrary, finalizers.)

Q95. [Advanced] (Theory) When is spawning an isolate the wrong choice for non-I/O work?

Hint

Overhead vs work.

Solution

When the work is small relative to spawn + message-copy overhead — the isolate can be slower than inline. Isolates pay off only when the computation clearly dwarfs that cost.


Section I — Zones (Q96–100)

Q96. [Basic] (Theory) In one sentence, what is a zone?

Hint

Context across async gaps. Part 9.

Solution

An execution context that stays attached to your code across asynchronous gaps, so behaviors like error handling, print, and context values apply to all callbacks that code later schedules.

Q97. [Basic] (Coding) Catch an uncaught async error with a zone.

Hint

runZonedGuarded.

Solution
runZonedGuarded(() { Future(() => throw 'oops'); },
  (e, s) => print('reported: $e'));

Q98. [Medium] (Theory) Why can a zone catch what a try/catch can't?

Hint

Sync vs async scope.

Solution

try/catch only guards code in the current synchronous call stack. Async callbacks run later on fresh stacks with no try/catch. A zone stays attached to all callbacks scheduled within it, so its handler still applies whenever they throw.

Q99. [Medium] (Coding) Propagate a requestId to a deep helper without passing it as a parameter.

Hint

zone-local values.

Solution
runZoned(() => deepHelper(), zoneValues: {#requestId: 'r1'});
// deepHelper: print(Zone.current[#requestId]);

Q100. [Advanced] (Theory) How does Flutter use zones, and which two error channels do you wire for full crash reporting?

Hint

Framework + async.

Solution

Flutter runs your app in a zone so no async error disappears. Wire FlutterError.onError (framework/synchronous errors) and runZonedGuarded's onError (or PlatformDispatcher.instance.onError) for uncaught async errors — both routed to your reporter (Crashlytics/Sentry).


Coding Mini-Exercises

Ten slightly larger problems that combine multiple parts. Write and run each, then compare with the solution. They escalate in difficulty.

Exercise 1 — Retry with backoff. Write Future<T> retry<T>(Future<T> Function() task, {int attempts = 3}) that retries a failing task up to attempts times, waiting 200ms × attempt between tries, and rethrows the last error.

Show solution
Future<T> retry<T>(Future<T> Function() task, {int attempts = 3}) async {
  for (var i = 1; ; i++) {
    try {
      return await task();
    } catch (e) {
      if (i >= attempts) rethrow;            // out of attempts → propagate
      await Future.delayed(Duration(milliseconds: 200 * i)); // backoff
    }
  }
}

Combines async/await (Part 3), error handling + rethrow (Part 7), and Future.delayed (Part 2).

Exercise 2 — Parallel map with concurrency limit. Run an async worker over a list but at most n at a time.

Show solution
Future<List<R>> mapPooled<T, R>(
    List<T> items, Future<R> Function(T) worker, {int n = 4}) async {
  final results = List<R?>.filled(items.length, null);
  var index = 0;

  Future<void> runner() async {
    while (true) {
      final i = index++;          // claim the next item
      if (i >= items.length) return;
      results[i] = await worker(items[i]);
    }
  }

  await Future.wait(List.generate(n, (_) => runner())); // n workers in flight
  return results.cast<R>();
}

Future.wait (Part 2) runs n "runner" loops concurrently; each pulls from a shared index. A real-world throttle.

Exercise 3 — Debounce a stream. Emit a value only after the source has been quiet for 300ms (great for search boxes).

Show solution
Stream<T> debounce<T>(Stream<T> source, Duration d) {
  late StreamController<T> controller;
  Timer? timer;
  controller = StreamController<T>(
    onListen: () {
      source.listen((value) {
        timer?.cancel();                       // reset the quiet timer
        timer = Timer(d, () => controller.add(value));
      }, onError: controller.addError, onDone: () {
        timer?.cancel();
        controller.close();
      });
    },
    onCancel: () => timer?.cancel(),
  );
  return controller.stream;
}

Uses StreamController lifecycle (Part 5) and a Timer. Each new event cancels the pending emit; only a 300ms gap lets one through.

Exercise 4 — Merge two streams. Combine two Stream<int>s into one that emits from whichever fires.

Show solution
Stream<T> merge<T>(Stream<T> a, Stream<T> b) async* {
  final controller = StreamController<T>();
  final subs = [a.listen(controller.add), b.listen(controller.add)];
  var open = 2;
  for (final s in subs) {
    s.onDone(() { if (--open == 0) controller.close(); });
  }
  yield* controller.stream;                    // re-emit the merged stream
}

Combines StreamController (Part 5) with async* + yield* (Part 6). (Production code: use package:rxdart's MergeStream.)

Exercise 5 — Timeout wrapper. Implement Future<T> withTimeout<T>(Future<T> f, Duration d) that completes with f or throws TimeoutException.

Show solution
Future<T> withTimeout<T>(Future<T> f, Duration d) {
  return f.timeout(d); // built-in; throws TimeoutException on expiry
}
// Manual version with Completer, for understanding:
Future<T> manualTimeout<T>(Future<T> f, Duration d) {
  final c = Completer<T>();
  final timer = Timer(d, () {
    if (!c.isCompleted) c.completeError(TimeoutException('timed out', d));
  });
  f.then((v) { if (!c.isCompleted) c.complete(v); },
         onError: (e, s) { if (!c.isCompleted) c.completeError(e, s); })
   .whenComplete(timer.cancel);
  return c.future;
}

The manual version shows Completer (Part 2) racing a TimerisCompleted guards the "complete once" rule.

Exercise 6 — Async generator paging. Stream all items from a paginated API lazily, stopping when the consumer stops.

Show solution
Stream<Item> allItems(Future<List<Item>> Function(int page) getPage) async* {
  for (var page = 1; ; page++) {
    final batch = await getPage(page);   // await between yields
    if (batch.isEmpty) return;           // closes the stream
    for (final item in batch) yield item;
  }
}
// Consumer pulls only 30 → fetching stops once 30 are yielded:
await for (final item in allItems(api.getPage).take(30)) { render(item); }

Pure async* laziness (Part 6) — take(30) cancels the generator, so no extra pages are fetched.

Exercise 7 — Offload + report errors. Run a heavy parse on an isolate, and ensure any failure is reported, not swallowed.

Show solution
Future<Result> parseHeavy(String raw) async {
  try {
    return await Isolate.run(() => expensiveParse(raw)); // another thread
  } catch (e, s) {
    reporter.record(e, s);  // log/report
    rethrow;                // let caller/UI react
  }
}

Isolate.run (Part 8) for parallelism; try/catch + rethrow (Part 7) because errors thrown inside the isolate propagate back through the returned future.

Exercise 8 — Predict-and-explain. Without running, give the exact output and explain each line's timing:

import 'dart:async';
void main() {
  print('1');
  Future(() => print('2')).then((_) => print('3'));
  scheduleMicrotask(() => print('4'));
  Future.value().then((_) => print('5'));
  print('6');
}
Show solution
1
6
4
5
2
3

1,6 sync. Microtask queue drains next: scheduleMicrotask4, then Future.value().then5 (already-complete future schedules its .then as a microtask). Then the event queue: Future(...)2, whose .then (3) is queued as a microtask and runs immediately after that event. Tests Sections A & B together.

Exercise 9 — Safe one-shot. Build a Once<T> that runs an async initializer at most once, returning the cached future to all callers (a lazy singleton).

Show solution
class Once<T> {
  final Future<T> Function() _init;
  Future<T>? _future;
  Once(this._init);

  Future<T> get value => _future ??= _init(); // start once; reuse the future
}

// Usage: every caller awaits the SAME in-flight future, so _init runs once.
final config = Once(() => loadConfigFromDisk());

The ??= ensures _init() is invoked only on the first access; concurrent callers all receive the same Future (no duplicate work). Connects to "a future completes once" (Part 2).

Exercise 10 — Capstone: a mini job runner. Build a JobRunner that: (1) accepts jobs via a method, (2) processes them one at a time in order on a background isolate, (3) exposes a broadcast stream of results, (4) reports errors without stopping the queue, and (5) cleans up on dispose.

Show solution
import 'dart:async';
import 'dart:isolate';

class JobResult {
  final int id;
  final Object? value;
  final Object? error;
  JobResult.ok(this.id, this.value) : error = null;
  JobResult.fail(this.id, this.error) : value = null;
}

class JobRunner {
  final _results = StreamController<JobResult>.broadcast(); // (3) results out
  final _queue = StreamController<MapEntry<int, int>>();    // incoming jobs
  late final StreamSubscription _sub;
  var _nextId = 0;

  JobRunner() {
    // (2) process sequentially: await each job before taking the next.
    _sub = _queue.stream.listen((entry) async {
      final id = entry.key, input = entry.value;
      try {
        final out = await Isolate.run(() => _heavy(input)); // (1)+(2) off-thread
        _results.add(JobResult.ok(id, out));
      } catch (e) {
        _results.add(JobResult.fail(id, e));                // (4) report, keep going
      }
    });
  }

  Stream<JobResult> get results => _results.stream;

  int submit(int input) {
    final id = _nextId++;
    _queue.add(MapEntry(id, input));
    return id; // caller can match results by id
  }

  Future<void> dispose() async {                            // (5) cleanup
    await _sub.cancel();
    await _queue.close();
    await _results.close();
  }

  static int _heavy(int n) {                                 // top-level-ish work
    var t = 0;
    for (var i = 0; i < n; i++) t += i;
    return t;
  }
}

This single class exercises the whole series: a StreamController queue (Part 5), sequential await processing so jobs run in order (Part 3), Isolate.run for parallel compute (Part 8), a broadcast results stream (Part 4), try/catch that reports without killing the queue (Part 7), and disciplined dispose cleanup. If you can build and explain this, you've mastered async Dart.


You made it

Ten parts, one hundred questions, and a capstone. You now command the entire async surface of Dart:

Revisit any question you needed a hint for in a week. Then go build something that never drops a frame.