| |

Dart 52 🎯 Single-Threaded Concurrency Model and Microtask Queue Prioritization

⏱️ Estimated read: 17 min | |

Dart executes application code on a single thread. There is no parallelism in the sense that two pieces of Dart code run at the same instant on the same isolate. Yet Dart programs handle thousands of concurrent operations: network requests, timers, file reads, user input, and rendering frames. The mechanism that makes this possible is the event loop, a scheduler that runs one task at a time from a queue, switching between tasks as they become ready. The event loop is fed by two queues, the event queue and the microtask queue, and the priority rule between them is the single most important detail for understanding Dart’s execution order. Getting this rule right is the difference between code that runs when you expect and code that runs at a surprising moment.

Key point: The microtask queue has strict priority over the event queue. The event loop drains the entire microtask queue before processing the next event. Microtasks scheduled during microtask processing are also drained before the loop yields. A long microtask chain can starve the event queue and freeze the application.


Why the single-threaded model exists

The shared-state problem. Multiple threads sharing memory require locks, atomic operations, and careful synchronization. Every access to shared data is a potential race condition. The complexity of correct multithreaded code is enormous, and the bugs it produces are intermittent and difficult to reproduce. Dart avoids this class of problem by running each isolate on a single thread. There is no shared mutable state to protect.

The Isolate model. Dart does have concurrency, but it uses isolates rather than threads. An isolate is an independent unit of execution with its own memory heap and its own event loop. Isolates do not share memory; they communicate by sending messages. The result is that each isolate’s code is single-threaded, and the concurrency comes from running multiple isolates side by side.

The UI responsiveness requirement. Flutter applications must render frames every 16 milliseconds to stay smooth. If the main isolate blocks on a synchronous operation, frames are dropped. The event loop lets the application perform asynchronous work without blocking, so the frame rendering can be scheduled between other tasks.

The predictability requirement. A developer needs to know when a piece of code will run. The event loop’s ordering rules are deterministic: microtasks run before events, and within each queue tasks run in FIFO order. This determinism is what allows developers to reason about execution order without inspecting the runtime.

The performance requirement. Threads have overhead: context switches, stack allocation, and cache invalidation. For the common case, where work is I/O-bound rather than CPU-bound, a single-threaded event loop is more efficient than spawning threads. It avoids the overhead while still allowing concurrent I/O.

The framework integration requirement. Flutter’s rendering pipeline, the Dart VM, and the platform’s event sources (timers, I/O, user input) all feed events into the same loop. The loop is the point where all asynchronous activity converges, and the priority rules determine which activity happens first.


a. The event loop

The event loop is the scheduler that drives all asynchronous execution in a Dart isolate. It maintains two queues and processes them according to a fixed priority rule.

The two queues

The event queue holds external events: I/O completions, timer expirations, user input, messages from other isolates, and platform channel responses. The microtask queue holds internal callbacks: Future completions, scheduleMicrotask calls, and internal Dart VM work.

// Scheduling an event (indirectly, via a timer).
Timer(Duration.zero, () => print('event'));

// Scheduling a microtask (directly).
scheduleMicrotask(() => print('microtask'));

The distinction is the source: external events arrive from outside the isolate, and microtasks are scheduled by Dart code running inside the isolate.

The processing rule

The event loop’s rule is simple and absolute: drain the microtask queue completely, then process one event from the event queue, then drain the microtask queue again, then process the next event, and so on.

Event loop pseudo-code:
while (true) {
  while (microtask queue is not empty) {
    execute next microtask;
  }
  if (event queue is not empty) {
    execute next event;
  }
}

The inner while loop drains the microtask queue. The outer loop processes one event at a time. If a microtask schedules another microtask, the inner loop continues, and the event queue waits.

The starvation risk

Because the microtask queue is drained completely before any event runs, a microtask that schedules another microtask indefinitely will starve the event queue. The application’s timers, I/O, and input will never be processed, and the UI will freeze.

// BAD: infinite microtask loop.
void infiniteMicrotasks() {
  scheduleMicrotask(infiniteMicrotasks);
}

This code schedules a microtask that schedules another microtask, forever. The event loop never reaches the event queue. The application becomes unresponsive. The same risk applies to a Future chain that resolves without ever awaiting anything external.

The event queue sources

Events arrive from several sources. Timers schedule events when they expire. I/O operations schedule events when they complete. Platform channels schedule events when the platform responds. Isolate messages schedule events when they arrive. Each source produces events that are added to the back of the event queue.

// Timer event.
Timer(Duration(seconds: 1), () => print('timer'));

// Future from an I/O operation.
final file = await File('data.txt').readAsString();

// Platform channel event.
final result = await platform.invokeMethod('someMethod');

All of these eventually produce events, and all of them wait behind the microtask queue.


b. The microtask queue

The microtask queue is the high-priority queue. It runs before any event, and it runs until it is empty. Understanding what schedules microtasks and why is key to predicting execution order.

What schedules microtasks

Several Dart constructs schedule microtasks. The most common are Future completions and scheduleMicrotask.

When a Future completes, the callbacks registered with then and catchError are scheduled as microtasks. When an async function awaits a completed future, the continuation is scheduled as a microtask. When a Completer is completed, its listeners are scheduled as microtasks.

Future.microtask(() => print('microtask 1'));
scheduleMicrotask(() => print('microtask 2'));

Future(() => print('event 1')).then((_) => print('microtask 3'));

The Future(...) constructor schedules an event, not a microtask. The then callback on its completion is scheduled as a microtask. The Future.microtask constructor schedules a microtask directly.

The FIFO order within microtasks

Microtasks run in the order they were scheduled. If three microtasks are scheduled, they run in sequence. If a microtask schedules another microtask, the new one is added to the back of the queue and runs after the already-scheduled ones.

scheduleMicrotask(() => print('A'));
scheduleMicrotask(() => print('B'));
scheduleMicrotask(() => print('C'));
// Output: A, B, C
scheduleMicrotask(() {
  print('A');
  scheduleMicrotask(() => print('C'));
});
scheduleMicrotask(() => print('B'));
// Output: A, B, C
// C was scheduled during A's execution, so it goes behind B.

The second example shows the FIFO behavior. C is scheduled while A runs, but B is already in the queue, so B runs first.

The await and microtask interaction

An async function’s await expression schedules a microtask for the continuation. If the awaited future is already complete, the continuation is still scheduled as a microtask, not run synchronously.

Future<void> example() async {
  print('start');
  await Future.value();
  print('after await');
}

void main() {
  example();
  print('main');
}

// Output:
// start
// main
// after await

The await Future.value() yields control to the event loop, which schedules the continuation as a microtask. The print('main') runs before the continuation because the microtask queue runs after the current synchronous execution finishes.

The scheduleMicrotask use case

scheduleMicrotask is rarely needed in application code. Its primary use is in framework internals and low-level libraries where a callback must run after the current synchronous execution but before any event. A common pattern is to coalesce multiple synchronous changes into a single microtask.

bool _scheduled = false;

void scheduleUpdate() {
  if (_scheduled) return;
  _scheduled = true;
  scheduleMicrotask(() {
    _scheduled = false;
    performUpdate();
  });
}

Multiple calls to scheduleUpdate during the same synchronous block result in one performUpdate call, because the microtask is scheduled only once.


c. Execution order in practice

The interaction between synchronous code, microtasks, and events determines the observable order of output. The rules are deterministic, and learning to trace them removes the guesswork from asynchronous code.

The ordering rule

The complete ordering is: synchronous code runs first, then microtasks, then events. Within each category, FIFO order applies. Microtasks scheduled by microtasks are drained before events. Events scheduled by microtasks are added to the back of the event queue.

void main() {
  print('1: synchronous');

  Future(() => print('4: event')).then((_) => print('5: microtask from event'));

  scheduleMicrotask(() => print('2: microtask'));

  Future.microtask(() => print('3: microtask from Future.microtask'));

  print('1b: synchronous');
}

// Output:
// 1: synchronous
// 1b: synchronous
// 2: microtask
// 3: microtask from Future.microtask
// 4: event
// 5: microtask from event

The synchronous prints happen first. The two microtasks run next in the order they were scheduled. The event runs last, and the microtask scheduled by the event runs after it.

The async function tracing

An async function executes synchronously until its first await. The await suspends the function and schedules the rest as a microtask. This is why an async function’s body before the first await runs synchronously, and everything after runs later.

Future<void> example() async {
  print('A: before await');
  await Future.delayed(Duration.zero);
  print('C: after await');
}

void main() {
  print('1: main start');
  example();
  print('2: main end');
}

// Output:
// 1: main start
// A: before await
// 2: main end
// C: after await

The A print runs synchronously because it is before the await. The C print runs after the microtask and event queues have processed the timer.

The await Future.delayed behavior

Future.delayed schedules an event, not a microtask. This is why it waits for the event queue, which means it runs after all microtasks have drained.

Future<void> example() async {
  print('start');
  await Future.delayed(Duration.zero);
  print('after delay');
}

void main() {
  example();
  scheduleMicrotask(() => print('microtask'));
  Future(() => print('event'));
}

// Output:
// start
// microtask
// event
// after delay

The microtask runs before the event because microtasks have priority. The event runs before after delay because the timer’s event was scheduled before the delayed future’s event, and both are events. The after delay continuation runs after the timer event’s handler.

The nested await case

When async functions await each other, the continuations form a chain of microtasks. Each await adds one more microtask to the queue.

Future<void> a() async {
  print('a start');
  await b();
  print('a end');
}

Future<void> b() async {
  print('b start');
  await Future.value();
  print('b end');
}

void main() {
  a();
  print('main');
}

// Output:
// a start
// b start
// main
// b end
// a end

The a start and b start run synchronously. The main print runs next. Then the microtasks drain: b end runs first because it was scheduled first, and then a end runs when b’s future completes.


Complete Example Session

// ============================================
// PART 1: SYNCHRONOUS CODE FIRST
// ============================================

void main() {
  print('1: sync');
  Future(() => print('4: event'));
  scheduleMicrotask(() => print('2: microtask'));
  print('1b: sync');
}

// Output:
// 1: sync
// 1b: sync
// 2: microtask
// 4: event

// Synchronous code runs to completion.
// Then microtasks.
// Then events.

// ============================================
// PART 2: MICROTASK PRIORITY
// ============================================

void main() {
  Future(() => print('event'));
  scheduleMicrotask(() => print('microtask'));
}

// Output:
// microtask
// event

// Microtasks always run before events,
// regardless of scheduling order.

// ============================================
// PART 3: MULTIPLE MICROTASKS IN FIFO ORDER
// ============================================

void main() {
  scheduleMicrotask(() => print('A'));
  scheduleMicrotask(() => print('B'));
  scheduleMicrotask(() => print('C'));
}

// Output:
// A
// B
// C

// FIFO within the microtask queue.

// ============================================
// PART 4: MICROTASK SCHEDULING MICROTASK
// ============================================

void main() {
  scheduleMicrotask(() {
    print('A');
    scheduleMicrotask(() => print('C'));
  });
  scheduleMicrotask(() => print('B'));
}

// Output:
// A
// B
// C

// C was scheduled during A's execution,
// so it goes behind B, which was already queued.

// ============================================
// PART 5: THE AWAIT CONTINUATION
// ============================================

Future<void> example() async {
  print('start');
  await Future.value();
  print('after await');
}

void main() {
  example();
  print('main');
}

// Output:
// start
// main
// after await

// The await schedules the continuation as a microtask.
// The synchronous code after the call runs first.

// ============================================
// PART 6: FUTURE.DELAYED IS AN EVENT
// ============================================

void main() {
  Future.delayed(Duration.zero, () => print('delayed'));
  scheduleMicrotask(() => print('microtask'));
}

// Output:
// microtask
// delayed

// Future.delayed schedules an event, not a microtask.
// Microtasks have priority.

// ============================================
// PART 7: EVENT + MICROTASK INTERLEAVING
// ============================================

void main() {
  Future(() {
    print('event 1');
    scheduleMicrotask(() => print('microtask from event 1'));
  });
  Future(() => print('event 2'));
}

// Output:
// event 1
// microtask from event 1
// event 2

// The microtask scheduled by event 1 runs before event 2.
// Microtasks are drained after each event.

// ============================================
// PART 8: STARVATION
// ============================================

void infiniteMicrotasks() {
  scheduleMicrotask(infiniteMicrotasks);
}

void main() {
  infiniteMicrotasks();
  Timer(Duration(seconds: 1), () => print('timer'));
  // The timer will never fire.
  // The event queue is starved.
}

// This code freezes the application.
// The microtask queue never empties.

// ============================================
// PART 9: COALESCING UPDATES WITH MICROTASKS
// ============================================

bool _scheduled = false;

void scheduleUpdate() {
  if (_scheduled) return;
  _scheduled = true;
  scheduleMicrotask(() {
    _scheduled = false;
    print('update performed');
  });
}

void main() {
  scheduleUpdate();
  scheduleUpdate();
  scheduleUpdate();
  print('sync done');
}

// Output:
// sync done
// update performed

// Three calls to scheduleUpdate result in one update.
// The microtask runs after the synchronous code completes.

// ============================================
// PART 10: THE COMPLETE ORDERING RULE
// ============================================

// For any Dart program:
//
// 1. Synchronous code runs to completion.
// 2. Microtasks drain completely (FIFO).
//    - Microtasks scheduled during draining are also drained.
//    - This continues until the queue is empty.
// 3. One event is processed.
// 4. Microtasks drain again.
// 5. Next event.
// 6. Repeat.
//
// There is no preemption.
// There is no parallelism within an isolate.
// The event loop is the only scheduler.

The ten parts progressed from synchronous execution through microtask priority, FIFO ordering, nested microtasks, await continuations, Future.delayed as an event, interleaving, starvation, coalescing, and the complete ordering rule. The same examples ran through every part, showing how the two queues interact.


Quick Reference

Queue Priority

QueuePrioritySource
MicrotaskHighFuture completions, scheduleMicrotask
EventLowTimers, I/O, messages, platform channels

Scheduling Constructs

ConstructQueueTiming
Future(...)EventNext event cycle
Future.microtask(...)MicrotaskBefore next event
Future.value(...)Microtask (completion)Before next event
Future.delayed(...)EventAfter duration
scheduleMicrotask(...)MicrotaskBefore next event
awaitMicrotask (continuation)Before next event
Timer(...)EventAfter duration

Execution Order

PhaseWhat Runs
1Synchronous code
2All microtasks (FIFO)
3One event
4All microtasks again
5Next event
6Repeat

Starvation Risk

PatternRisk
Microtask scheduling microtaskStarvation
await in a loop with completed futuresHigh microtask load
Recursive Future.microtaskStarvation
Long synchronous codeBlocks everything

Best Practices

✅ Do This:

// Use Future for work that can wait for the next event cycle.
Future(() => doWork());  // ✅

// Use scheduleMicrotask to coalesce synchronous updates.
scheduleMicrotask(() => applyUpdates());  // ✅

// Keep microtasks short.
scheduleMicrotask(() => { /* brief work */ });  // ✅

// Use await for sequencing async operations.
await doFirst();
await doSecond();  // ✅

// Let the event loop handle I/O and timers.
final data = await fetchData();  // ✅

❌ Don’t Do This:

// Don't schedule microtasks in a loop without a break.
scheduleMicrotask(infiniteMicrotasks);  // ❌ Starvation

// Don't do heavy work in a microtask.
scheduleMicrotask(() => processMillionItems());  // ❌ Blocks events

// Don't block the event loop with synchronous I/O.
final data = File('big.txt').readAsStringSync();  // ❌ Freezes UI

// Don't assume Future(() => ...) runs before microtasks.
Future(() => print('event'));  // ❌ Runs after all microtasks

// Don't use scheduleMicrotask for work that can wait.
scheduleMicrotask(() => updateUI());  // ❌ Use a Future if it can wait

Common Pitfalls

PitfallWhy It HappensFix
UI freezesMicrotask queue never emptiesAvoid microtask loops
Timers never fireEvent queue starvedEnsure microtasks complete
Wrong output orderConfusing microtasks and eventsRemember: microtasks first
Heavy work blocks framesLong synchronous codeOffload to isolate or split
await appears synchronousMisunderstanding async suspensionawait schedules a microtask
Future.delayed ordering surpriseAssuming it is a microtaskIt is an event
Coalescing not workingMicrotask not scheduledCheck the flag logic

Real-World Examples

1. Synchronous First

print('sync');
scheduleMicrotask(() => print('micro'));
// Output: sync, micro

2. Microtask Before Event

Future(() => print('event'));
scheduleMicrotask(() => print('micro'));
// Output: micro, event

3. FIFO Microtasks

scheduleMicrotask(() => print('A'));
scheduleMicrotask(() => print('B'));
// Output: A, B

4. await Continuation

await Future.value();
print('after');  // Runs as a microtask.

5. Future.delayed Is an Event

Future.delayed(Duration.zero, () => print('delayed'));
scheduleMicrotask(() => print('micro'));
// Output: micro, delayed

6. Microtask from Event

Future(() {
  print('event');
  scheduleMicrotask(() => print('micro'));
});
// Output: event, micro

7. Coalescing Updates

if (!_scheduled) {
  _scheduled = true;
  scheduleMicrotask(() { _scheduled = false; update(); });
}

8. Starvation

void loop() { scheduleMicrotask(loop); }
loop();  // Freezes the app.

9. Avoid Blocking

// BAD: readAsStringSync blocks the event loop.
final data = File('big.txt').readAsStringSync();
// GOOD:
final data = await File('big.txt').readAsString();

10. Isolate for Heavy Work

final result = await compute(heavyFunction, data);
// Runs on a separate isolate, not the main event loop.

Visual

The Event Loop

┌────────────────────────────────────────────────────────────┐
│  DART EVENT LOOP                                           │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  MICROTASK QUEUE (high priority)                  │     │
│  │  ┌─────┐ ┌─────┐ ┌─────┐                         │     │
│  │  │ MT1 │ │ MT2 │ │ MT3 │ → drain completely      │     │
│  │  └─────┘ └─────┘ └─────┘                         │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        │  Empty?                           │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  EVENT QUEUE (low priority)                       │     │
│  │  ┌─────┐ ┌─────┐ ┌─────┐                         │     │
│  │  │ E1  │ │ E2  │ │ E3  │ → one at a time         │     │
│  │  └─────┘ └─────┘ └─────┘                         │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        │  Process one                     │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  Back to microtask queue.                         │     │
│  │  Drain it again.                                  │     │
│  │  Process next event.                              │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The microtask queue always empties before an event runs.  │
│                                                            │
└────────────────────────────────────────────────────────────┘

Execution Order

┌────────────────────────────────────────────────────────────┐
│  EXECUTION ORDER FOR A DART PROGRAM                        │
│                                                            │
│  TIME ──────────────────────────────────────────────▶      │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  1. SYNCHRONOUS CODE                              │     │
│  │     Runs to completion.                           │     │
│  │     main() body, constructors, top-level code.    │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  2. MICROTASKS (FIFO)                             │     │
│  │     Future.then callbacks                         │     │
│  │     await continuations                           │     │
│  │     scheduleMicrotask callbacks                   │     │
│  │     Microtasks scheduled during draining run here.│     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  3. ONE EVENT                                     │     │
│  │     Timer, I/O completion, message, input.        │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  4. MICROTASKS AGAIN                              │     │
│  │     Drain completely.                             │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  5. NEXT EVENT                                    │     │
│  │     Repeat from step 3.                           │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
└────────────────────────────────────────────────────────────┘

Microtask vs Event Sources

┌────────────────────────────────────────────────────────────┐
│  WHAT SCHEDULES MICROTASKS vs EVENTS                       │
│                                                            │
│  MICROTASKS (high priority)                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  scheduleMicrotask(callback)                      │     │
│  │  Future.microtask(callback)                       │     │
│  │  Future.value().then(callback)                    │     │
│  │  await (continuation)                             │     │
│  │  Completer.complete() listeners                   │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  EVENTS (low priority)                                     │
│  ┌──────────────────────────────────────────────────┐     │
│  │  Future(callback)                                 │     │
│  │  Future.delayed(duration, callback)               │     │
│  │  Timer(duration, callback)                        │     │
│  │  File I/O completion                              │     │
│  │  Network response                                 │     │
│  │  Platform channel reply                           │     │
│  │  Isolate message                                  │     │
│  │  User input                                       │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  Microtasks come from inside the isolate.                  │
│  Events come from outside the isolate.                     │
│                                                            │
└────────────────────────────────────────────────────────────┘

Starvation

┌────────────────────────────────────────────────────────────┐
│  STARVATION: MICROTASK QUEUE NEVER EMPTIES                 │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  scheduleMicrotask(() => {                        │     │
│  │    scheduleMicrotask(() => {                      │     │
│  │      scheduleMicrotask(() => { ... })             │     │
│  │    })                                             │     │
│  │  })                                               │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  MICROTASK QUEUE                                  │     │
│  │  ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ... forever    │     │
│  │  │ MT1 │ │ MT2 │ │ MT3 │ │ MT4 │                 │     │
│  │  └─────┘ └─────┘ └─────┘ └─────┘                 │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        X  Never reached                    │
│                        │                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  EVENT QUEUE                                      │     │
│  │  ┌─────┐ ┌─────┐ ┌─────┐                         │     │
│  │  │ E1  │ │ E2  │ │ E3  │                         │     │
│  │  └─────┘ └─────┘ └─────┘                         │     │
│  │  Timers, I/O, input: all starved.                 │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The application freezes. The UI never updates.            │
│                                                            │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
Event loopScheduler driving all async execution
Microtask queueHigh-priority queue for internal callbacks
Event queueLow-priority queue for external events
Priority ruleMicrotasks drain completely before any event
Synchronous firstSynchronous code runs before either queue
FIFOBoth queues process in first-in, first-out order
Future(...)Schedules an event
Future.microtask(...)Schedules a microtask
Future.delayed(...)Schedules an event
scheduleMicrotask(...)Schedules a microtask
awaitSuspends, schedules continuation as microtask
StarvationMicrotask loop blocks all events
IsolateIndependent heap and event loop

Key takeaways:

  • Dart runs on a single thread per isolate. There is no parallelism within an isolate. Concurrency comes from the event loop scheduling one task at a time and switching as tasks become ready.
  • The microtask queue has strict priority. The event loop drains the entire microtask queue before processing the next event. This is the single most important rule for predicting execution order.
  • Synchronous code runs first. Before either queue is processed, the current synchronous block runs to completion. This is why main() body code runs before any scheduled callback.
  • Microtasks are scheduled by await, Future completions, and scheduleMicrotask. These callbacks run before any timer, I/O, or platform event.
  • Events come from outside the isolate. Timers, I/O completions, platform channel responses, and isolate messages are events. They wait behind the microtask queue.
  • Future(...) schedules an event, not a microtask. This is a common source of confusion. The completion callbacks registered on the future are microtasks, but the future’s own execution is an event.
  • The FIFO order is preserved within each queue. Microtasks run in the order they were scheduled. Events run in the order they arrived. A microtask scheduled during microtask processing goes to the back of the queue.
  • Microtask starvation freezes the application. A microtask that schedules another microtask indefinitely prevents the event queue from ever being processed. Timers, I/O, and user input are blocked.
  • Keep microtasks short. A long-running microtask blocks all events, including frame rendering. Heavy work belongs in an isolate or should be split across events.

Remember: Dart’s concurrency is built on a single-threaded event loop per isolate, with two queues. The microtask queue holds internal callbacks: Future completions, await continuations, and explicit scheduleMicrotask calls. The event queue holds external events: timers, I/O, platform messages, and user input. The loop drains the microtask queue completely before processing one event, then drains microtasks again, then processes the next event. Synchronous code always runs first, before either queue. The ordering is deterministic and the priority is absolute. Understanding this rule removes the guesswork from asynchronous code and reveals the one failure mode to avoid: a microtask chain that never ends, which starves the event queue and freezes the application.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!