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()orFuture.microtask().
- Microtask queue: For short, high‑priority tasks that should run as soon as possible, before any event queue tasks. Tasks are added via
- Event queue: For tasks that come from external events (I/O, timers, user interactions) and from
Futureconstructors (likeFuture(() {})orFuture.delayed).
- Event queue: For tasks that come from external events (I/O, timers, user interactions) and from
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
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.
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.
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
Key Takeaways
- Dart uses an event loop with two queues: microtask and event.
- Microtasks run before any event queue tasks.
- Use
scheduleMicrotaskorFuture.microtaskfor short, high‑priority work.
- Use
- Most async code should use the event queue (default
Future).
- Most async code should use the event queue (default
async/awaitschedules continuations as microtasks.
- Avoid flooding the microtask queue to keep the app responsive.