flutter
/

Dart Event Loop – How Asynchronous Code Works

Last Sync: Today

On this page

10
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Dart Event Loop – How Asynchronous Code Works

What is the Event Loop?

Dart is a single‑threaded language, but it can handle asynchronous operations efficiently using an event loop. The event loop continuously checks two queues: the microtask queue and the event queue. When the current synchronous code finishes, the event loop picks tasks from these queues and executes them. This mechanism allows Dart to perform I/O, timers, and other async work without blocking the main thread.

The Two Queues

    • Microtask queue: For short, high‑priority tasks that should run as soon as possible, before any event queue tasks. Tasks are added via scheduleMicrotask() or Future.microtask().
    • Event queue: For tasks that come from external events (I/O, timers, user interactions) and from Future constructors (like Future(() {}) or Future.delayed).

The event loop processes tasks in this order: it runs all microtasks until the microtask queue is empty, then takes one event from the event queue, runs it, and repeats.

Execution Order Example

DARTRead-only
1
void main() {
  print('1');
  Future(() => print('2')); // event queue
  Future.microtask(() => print('3')); // microtask queue
  scheduleMicrotask(() => print('4')); // microtask queue
  Future(() => print('5')); // event queue
  print('6');
}
// Output: 1, 6, 3, 4, 2, 5

Synchronous code runs first. Then microtasks run before any event queue tasks. Both Future.microtask and scheduleMicrotask add to the microtask queue.

How Futures Are Scheduled

The Future constructor takes a callback that is placed on the event queue. However, there are other ways to create Futures that affect where the callback runs:

    • Future(() {}) – schedules on the event queue.
    • Future.microtask(() {}) – schedules on the microtask queue.
    • Future.sync(() {}) – runs synchronously, not scheduled.
    • Future.value(x) – creates a completed Future; its then callback is scheduled as a microtask.
    • Future.error(e) – also schedules its then callbacks as microtasks.
DARTRead-only
1
void main() {
  print('Start');
  Future.value(42).then((v) => print('Value: $v')); // microtask
  Future(() => print('Event')); // event
  Future.microtask(() => print('Micro')); // microtask
  print('End');
}
// Output: Start, End, Value: 42, Micro, Event

Microtasks vs Events: When to Use Which

Use microtasks for very short, high‑priority work that must run before any pending I/O or timers. For example, you might use them to clean up state or flush a buffer after a synchronous operation. However, avoid using them heavily because they can delay event handling and make the UI unresponsive.

Most async code should use the event queue (the default Future constructor) because it yields to other events and keeps the app responsive.

Why Not Use Microtasks for Everything?

If you schedule a large number of microtasks, the event loop will process them all before handling any events (like user input or rendering). This can freeze the UI. Therefore, only use microtasks for tasks that are guaranteed to be fast and few.

The async/await Sugar

When you mark a function async and use await, the code after the await is scheduled as a microtask. This ensures that after the awaited Future completes, the continuation runs before any new events.

DARTRead-only
1
void main() async {
  print('A');
  await Future(() => print('B')); // B is scheduled on event queue
  print('C'); // scheduled as microtask after B completes
  print('D'); // still synchronous inside the continuation
}
// Output: A, B, C, D

Common Pitfalls

    • Assuming Futures run in parallel: Futures are scheduled and executed in order (event queue). They do not run concurrently; they interleave with microtasks.
    • Heavy microtasks: Can starve the event queue and cause delays.
    • Not understanding microtask vs event queue order: Leads to subtle timing bugs.
    • Creating many microtasks in a loop: Can cause the UI to freeze.

Complete Example

DARTRead-only
1
void main() async {
  print('1: Start');

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

  Future(() => print('3: Event 1'));

  Future.microtask(() => print('4: Microtask via Future'));

  Future(() => print('5: Event 2')).then((_) => print('6: Then of Event 2'));

  await Future(() => print('7: Await event'));
  print('8: After await');

  print('9: End of main');
}
// Output:
// 1: Start
// 2: Microtask
// 4: Microtask via Future
// 3: Event 1
// 5: Event 2
// 6: Then of Event 2
// 7: Await event
// 8: After await
// 9: End of main

Key Takeaways

    • Dart uses an event loop with two queues: microtask and event.
    • Microtasks run before any event queue tasks.
    • Use scheduleMicrotask or Future.microtask for short, high‑priority work.
    • Most async code should use the event queue (default Future).
    • async/await schedules continuations as microtasks.
    • Avoid flooding the microtask queue to keep the app responsive.

Try it yourself

void main() {
  print('Start');
  Future(() => print('Event'));
  scheduleMicrotask(() => print('Microtask'));
  print('End');
}

Test Your Knowledge

Q1
of 4

What is the correct execution order?

A
Microtasks → Synchronous → Events
B
Synchronous → Microtasks → Events
C
Events → Microtasks → Synchronous
D
Synchronous → Events → Microtasks
Q2
of 4

Which of these adds a task to the microtask queue?

A
Future(() => ...)
B
Future.delayed(() => ...)
C
Future.microtask(() => ...)
D
Timer.run(() => ...)
Q3
of 4

What is the output of this code? void main() { Future(() => print('1')); scheduleMicrotask(() => print('2')); print('3'); }

A
3 2 1
B
3 1 2
C
2 3 1
D
1 2 3
Q4
of 4

Why is it generally recommended to use event queue for most asynchronous work?

A
It's faster
B
It allows I/O and timers to be processed
C
Microtasks are not available in some environments
D
Events are guaranteed to run immediately

Frequently Asked Questions

What is the difference between `scheduleMicrotask` and `Future.microtask`?

Both schedule a task on the microtask queue. scheduleMicrotask is a top‑level function. Future.microtask returns a Future that completes when the task finishes, allowing you to chain .then or use await. They have the same effect on the queue order.

Does `Future(() {})` always go to the event queue?

Yes, the unnamed Future constructor places its callback on the event queue. However, if the Future is already completed (e.g., Future.value), its .then callbacks are scheduled as microtasks.

Can I rely on the exact order of execution in complex async code?

The order is deterministic according to the rules: synchronous code first, then all microtasks, then one event, then all microtasks again, etc. But for complex chaining, it's safer to design code to be order‑independent or to use well‑understood patterns.

What happens if a microtask throws an exception?

The exception is handled by the current zone's error handler. Uncaught exceptions can terminate the program. Always handle errors in microtasks with try/catch.

Why does `Future.value` schedule its then callbacks as microtasks?

This ensures that callbacks are always called asynchronously, even if the Future is already completed. It provides consistency: .then is never called synchronously, preventing unexpected re‑entrancy bugs.

Previous

dart isolates

Next

dart exception handling

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.