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 Timer — isCompleted 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: scheduleMicrotask→4, then Future.value().then→5 (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:
- The event loop and why ordering is what it is.
- Futures and async/await for waiting without blocking.
- Streams, StreamController, and generators for sequences over time.
- Error handling that actually catches.
- Isolates for true parallelism and zones for app-wide error boundaries.
Revisit any question you needed a hint for in a week. Then go build something that never drops a frame.