In Dart, how do you predict the print order when main() mixes synchronous prints, scheduleMicrotask, Future.microtask, Future(), Timer.run and then callbacks?
answer
- sort calls by queue first
- sync, then drain all microtasks
- one event, then drain again
- zero timers fire in creation order
- then on pending future runs at completion
basics
~20 sRun synchronous code first, then every queued microtask in scheduling order, then one event at a time with a full microtask drain after each. Future() and Timer.run are events; a then on a pending future runs the moment it completes.
solid answer
~40 sSort every call by where its callback lands. Top-level `print`s run now. `scheduleMicrotask`, `Future.microtask` and the completion of `Future.value` go on the microtask queue. `Timer.run` and `Future()` (built on `Timer.run`) are zero-duration timer events, fired in creation order. A `.then` on a still-pending future is a listener that runs synchronously when that future completes, so it beats any microtask its computation scheduled. Then apply the loop: sync code, drain all microtasks (including newly added ones), take one event, drain again. For a `main` that prints 1, queues `Timer.run(2)`, a `Future` printing 3 that schedules microtask 4 with `.then` printing 5, microtasks 6 and 7, `Future.value(8).then(print)`, and prints 9, the output is 1 9 6 7 8 2 3 5 4.
code
dart · 15 linesimport 'dart:async';
void main() {
scheduleMicrotask(() {
print('m1');
Timer.run(() => print('t2'));
scheduleMicrotask(() => print('m2'));
});
Timer.run(() {
print('t1');
scheduleMicrotask(() => print('m3'));
});
print('sync');
}
// sync, m1, m2, t1, m3, t2go deeper
Recall the order of the tiers: synchronous code, then microtasks, then timer events, and that Future() belongs to the timer tier.
Walk the procedure aloud: classify each call, drain microtasks fully, take one event, drain again, and explain why then on a pending future runs at completion.
Use the same model on real code: explain why a state change made in a then callback is visible before a microtask the computation queued, and where ordering assumptions in tests break.
Argue when code should depend on this ordering at all; relying on microtask-versus-timer order is fragile, and explicit awaits or completers usually state intent better.
## The puzzle interviewers write Ordering puzzles mix every scheduling API into one `main` and ask for the output. This is a typical one: ```dart import 'dart:async'; void main() { print('1'); Timer.run(() => print('2')); Future(() { print('3'); scheduleMicrotask(() => print('4')); }).then((_) => print('5')); Future.microtask(() => print('6')); scheduleMicrotask(() => print('7')); Future.value(8).then(print); print('9'); } // Output: 1 9 6 7 8 2 3 5 4 ``` Guessing from source order fails. Applying a fixed procedure does not. ## Step 1: sort every call by where its callback goes | Call | Where the callback goes | |---|---| | `print(...)` at top level | Runs now, synchronously | | `scheduleMicrotask(cb)` | Microtask queue | | `Future.microtask(fn)` | Microtask queue (it calls `scheduleMicrotask`) | | `Future.value(x)` with a non-future `x` | Its completion is a microtask; `.then` listeners run when it fires | | `Timer.run(cb)` | Event queue, as a zero-duration timer | | `Future(fn)` | Event queue, because it is implemented with `Timer.run` | | `.then` on a future that is still pending | Runs **synchronously when that future completes** | The last row is the one most candidates miss. A `.then` callback on a pending future is not queued anywhere; it is a listener, and completing the future runs it on the spot. ## Step 2: run the procedure 1. **Synchronous code first.** Everything `main` does directly: `1`, then `9`. While doing so it has queued three microtasks (`6`, `7`, the completion of `Future.value(8)`) and two zero-duration timers (`2`, then the `Future()` computation). 2. **Drain the microtask queue in scheduling order.** `6`, `7`, then the completion of `Future.value(8)`, which calls its `.then` listener: `8`. A microtask that schedules another microtask appends it to this same drain. 3. **Take the first event.** The `Timer.run` callback: `2`. Then drain microtasks — there are none. 4. **Take the next event.** The `Future()` computation prints `3` and schedules microtask `4`. When the computation returns, the future completes and its pending `.then` listener runs immediately: `5`. 5. **Drain microtasks again.** Only now does `4` run. Result: `1 9 6 7 8 2 3 5 4`. ## The rules the procedure encodes - **Synchronous code always finishes first.** No callback interrupts running Dart code. - **The whole microtask queue drains before any event**, including microtasks scheduled by other microtasks. - **Zero-duration timers fire in creation order**, each as its own event. On the Dart VM, pending microtasks are run after each timer callback, before the next timer is invoked. - **A timer created inside a microtask** joins the back of the event queue, behind timers created earlier. - **Completing a future runs its registered `.then` listeners at once**, as part of the code that completed it, which is why `5` beats `4`. - **A `.then` added to an already-completed future** is deferred to a microtask instead. ## Variations to practise 1. Swap the order of `Timer.run` and `Future(...)`: the two timer events swap, so the output becomes `1 9 6 7 8 3 5 4 2` — the `4` microtask now drains between the two events. 2. Add `scheduleMicrotask(() => Timer.run(() => print('10')))`: `10` prints last, because the timer is created during the first drain and lands behind both earlier timers. 3. Replace `Future(...)` with `Future.delayed(const Duration(milliseconds: 1), ...)`: it now waits for its wake-up time, so it still follows `2` but can also fall behind other events that arrive in the meantime. ## Common slips - **Treating `Future()` as "sooner" than `Timer.run`.** Both are zero-duration timers; only their creation order decides between them. - **Forgetting that `Future.value(x).then(...)` is asynchronous.** Its listener runs during the microtask drain, not at the line that registered it. - **Expecting a microtask queued inside a `Future()` computation to beat that future's own `.then`.** The listener runs as the future completes, still inside the event; the microtask waits for the drain that follows, which is why `5` precedes `4`. - **Counting `.then` as a separate queue entry.** On a pending future it is a listener; on a completed one it becomes a microtask. - **Ordering a non-zero delay by creation order.** A `Future.delayed` of even 1 ms waits for its wake-up time, so it cannot be placed against other events by source order alone. ## How to answer out loud State the procedure before the answer: "synchronous code, then every microtask in order, then one event at a time with a full microtask drain after each; `then` on a pending future runs when it completes." An interviewer is grading the model more than the digits, and the model lets you recover if you misread a line.
- Why does the then callback on a Future() computation run before a microtask that the computation itself scheduled?The `.then` listener is attached to a pending future. When the timer callback returns a value, completing the future runs its listeners immediately, still inside that event. The microtask the computation scheduled waits until the event handler, listeners included, has returned and the loop starts its drain.
- What changes if a microtask creates a Timer.run callback?The new timer joins the back of the event queue, behind every timer created earlier, so it fires after them even though its creator ran early. Microtasks that the same microtask schedules still run in the current drain, before any event.
saying these in an interview costs you the question
- Callbacks run in the order they appear in source code.
- Future() runs before Timer.run because futures are microtasks.
- A then callback always waits for a separate microtask.
- A microtask scheduled inside a microtask waits for the next loop turn.
- A timer created in a microtask jumps ahead of earlier timers.