skip to content

In Dart, which APIs put work on an isolate's microtask queue versus its event queue, and which queue does the event loop drain first?

level: middleimportance: must knowfreq 55%

answer

  1. one thread, one loop, two queues
  2. microtask queue emptied before next event
  3. scheduleMicrotask and Future.microtask
  4. Future() is built on Timer.run
  5. completed-future callbacks are microtasks

basics

~10 s

Each isolate's event loop empties the microtask queue before taking the next event. scheduleMicrotask, Future.microtask and callbacks on completed futures are microtasks; Future(), Timer.run, Future.delayed, I/O and port messages are events.

solid answer

~30 s

Each isolate has one thread running one event loop with two queues. The **event queue** holds timer callbacks (`Timer`, `Timer.run`, the computation of `Future()`, which is built on `Timer.run`, and `Future.delayed`), I/O completions and `ReceivePort` messages. The **microtask queue** holds `scheduleMicrotask` and `Future.microtask` callbacks, `.then` callbacks added to already-completed futures, the completion of `Future.value`, and `await` continuations on completed values. After the running code returns, the loop runs every microtask, including ones added during the drain, and only then takes the next event. So a microtask always beats a zero-duration timer queued alongside it, while `Future.sync` defers nothing at all.

code

dart · 16 lines
dart
import 'dart:async';

void main() {
  Future(() => print('Future()'));
  Timer.run(() => print('Timer.run'));
  Future.microtask(() => print('Future.microtask'));
  scheduleMicrotask(() => print('scheduleMicrotask'));
  Future.sync(() => print('Future.sync'));
  print('main done');
}
// Future.sync
// main done
// Future.microtask
// scheduleMicrotask
// Future()
// Timer.run

go deeper

for a junior

Recall the two queues and that microtasks always run before the next event, with scheduleMicrotask on one side and Timer on the other.

for a middle

Map each dart:async API to its queue, including the non-obvious ones: Future() is a timer, Future.value completes in a microtask, Future.sync runs in place.

for a senior

Use the rule to reason about real bugs: work that must precede input handling belongs in a microtask, work that must let input through belongs in a timer.

for a principal

Weigh the tradeoff when designing async libraries: microtask delivery keeps results prompt and uniform, but every microtask a library schedules delays the host's events.

## One event loop per isolate Every piece of Dart code runs inside an **isolate**: a unit with its own memory and a single thread. That thread runs one **event loop**, and the loop owns two queues: - the **event queue**, for work that arrives from outside the current code or after a delay: timer callbacks, I/O completions and messages on a `ReceivePort` (in a Flutter app's UI isolate, the engine's frame, input and platform-message callbacks likewise wait until the microtask queue is empty); - the **microtask queue**, for very short follow-up callbacks that Dart code schedules for "as soon as the current code finishes". Nothing runs in parallel inside one isolate. Each callback runs to completion before the next starts, so ordering is fully determined by which queue a callback lands in and when it was added. ## Which API lands where | API | Queue | What actually happens | |---|---|---| | `scheduleMicrotask(cb)` | microtask | Appends `cb` to the microtask queue | | `Future.microtask(fn)` | microtask | Calls `scheduleMicrotask` and completes the future with `fn`'s result | | `.then` on an already-completed future | microtask | Late listeners are notified in a microtask | | `Future.value(x)` with a non-future `x` | microtask | Completion itself is scheduled as a microtask | | `await` on a completed future or plain value | microtask | The continuation resumes in a microtask | | `Future(fn)` | event | Implemented with `Timer.run`, so `fn` runs as a timer event | | `Timer.run(cb)` | event | Documented as equivalent to `Timer(Duration.zero, cb)` | | `Future.delayed(d, fn)` | event | A `Timer`; with a zero duration it completes after all microtasks | | `Future.sync(fn)` | neither | Calls `fn` immediately and wraps the result or error | The last row matters in interviews: `Future.sync` does **not** defer anything. Only its result is delivered through a future. ## The loop's algorithm 1. Run the current synchronous code — `main`, or the event handler that is executing — to completion. 2. Run microtasks one at a time, in the order they were scheduled, **until the microtask queue is empty**. A microtask that schedules another microtask appends it to the same queue, and it runs in this same drain. 3. Take the next event from the event queue, run its handler to completion, and go back to step 2. On the Dart VM this is visible in the timer implementation: after each timer callback it runs the pending microtasks before invoking the next due timer, and each zero-duration timer is delivered as its own event in first-in, first-out order. So a microtask scheduled "after" a `Timer.run` in source order still runs first, and `Future.delayed(Duration.zero)` is documented to complete no sooner than the next event-loop iteration, after all microtasks have run. ## Why `dart:async` uses the microtask queue Futures need a way to deliver results **asynchronously but promptly**. If `Future.value(1).then(cb)` called `cb` synchronously, code would behave differently depending on whether a result happened to be ready. Scheduling the callback as a microtask keeps the rule uniform — callbacks never run inside the code that registers them — without paying for a trip behind every timer and I/O event already waiting. `Future.error` uses the same mechanism so that an error handler attached right after creation still catches the error. ## Misconceptions that cost points - **"`Future()` is a microtask."** It is a zero-duration timer, so it waits behind every pending microtask. - **"The loop alternates one microtask, one event."** It drains the whole microtask queue first. - **"Microtasks run on another thread."** Both queues are served by the isolate's one thread. - **"`Future.value(x).then(cb)` is synchronous."** `cb` runs in a microtask. - **"A zero-duration timer is immediate."** It is the earliest an *event* can run, never ahead of microtasks. ## Version notes - **`Future.pause([Duration duration = Duration.zero])`**, added in Dart 3.13, creates a future that completes from a timer with no computation attached. `await Future.pause()` is the clearest way to say "let queued events run, then continue". - **`Future.syncValue(value)`**, added in Dart 3.10, creates a future that is already complete at construction. Unlike `Future.value`, it does not accept a future as its value. Listeners added to it are still notified in a microtask, because they are late listeners on a completed future. - The ordering rules themselves have not changed across Dart 3 releases. ## When to use each Use `Future()` or `Timer.run` when you want other queued events — input, I/O, timers — to get a turn first. Use `scheduleMicrotask` or `Future.microtask` only for small follow-ups that must run before any other event is handled, and keep them short: a long chain of microtasks holds every event back.

  • Where does the callback in Future.value(1).then(cb) run?
    In a microtask. `Future.value` with a non-future argument schedules its own completion as a microtask, and `cb` runs as part of that completion. Adding `.then` to a future that has already completed is deferred to a microtask as well. Either way `cb` never runs inside the registering code, and it runs before any pending timer.
  • Is Future.sync the same as Future.microtask?
    No. `Future.sync` calls its computation immediately, in the current synchronous code, and only wraps the result or thrown error in a future. `Future.microtask` defers the computation to the microtask queue. Neither involves the event queue.
  • Does Future.delayed(Duration.zero) behave differently from Future()?
    Not in ordering. Both go through a zero-duration `Timer`, so both run as events after all pending microtasks, in creation order with other zero timers. `Future()` uses `Timer.run`, which is documented as `Timer(Duration.zero, callback)`. Since Dart 3.13, `Future.pause()` is the callback-free way to yield.

A deli counter with an express lane: before the clerk calls the next numbered ticket (an event), everyone in the express lane (microtasks) is served, including anyone who joins that lane while it is being served.

saying these in an interview costs you the question

  • Future() runs its computation as a microtask.
  • A Timer.run callback fires before microtasks scheduled after it.
  • The event loop alternates one microtask with one event.
  • Future.value(x).then runs its callback synchronously.
  • Microtasks run on a different thread from events.
  • Future.sync defers its computation to the microtask queue.