Understanding Dart’s Event Loop: Why Your Async Code Acts Weird

Table of contents
- What is an Event Loop?
- Dart has TWO types of queues:
- Execution Order: Microtasks vs Event Tasks
- Real-World Flutter Problem
- Problem: UI Not Updating After await
- Fix: Yield control using Future.delayed(Duration.zero)
- How Dart Awaits Work (Under the Hood)
- Use Case: Background Sync in a Flutter App
- Optimized Approach
- Best Practices for Dart’s Event Loop
- Conclusion
- Bonus Tip: Want to Explore Live?

Have you ever written asynchronous code in Flutter or Dart, and something just didn’t behave the way you expected?
Maybe you scheduled a Future
, but it didn’t run immediately.
Maybe your setState()
inside a Future.delayed()
broke the UI.
Maybe you used await
, but things still executed out of order.
If that sounds familiar welcome to the world of Dart’s Event Loop.
In this article, we’ll dive deep into how Dart’s asynchronous engine works not just to fix bugs, but to reason like the Dart runtime itself.
What is an Event Loop?
The Event Loop is the core of Dart’s asynchronous execution model. It powers:
Future
sasync
/await
UI updates
Timers and animations
When you write Dart code, it’s executed on a single thread, which means asynchronous code must be carefully scheduled and executed without blocking.
So how does Dart handle this?
Using two queues:
Dart has TWO types of queues:
Microtask Queue: High priority, runs before any event tasks.
Event Queue (aka Event Loop): Regular events like I/O, timer callbacks.
Execution Order: Microtasks vs Event Tasks
Here’s the general order of execution in Dart:
Run the current synchronous code
Run all microtasks (until the microtask queue is empty)
Run one event task
Repeat steps 2–3
Let’s make it visual 👇
import "dart:async";
void main() {
print('1');
scheduleMicrotask(() => print('2'));
Future(() => print('3'));
Future.delayed(Duration.zero, () => print('4'));
Future(() async {
print('5');
await Future.delayed(Duration.zero);
print('6');
});
scheduleMicrotask(() => print('7'));
print('8');
}
What gets printed?
1
8
2
7
3
4
5
6
Let’s break it down:
print('1')
: sync -> ImmediatelyscheduleMicrotask(...)
: Microtask -> After syncFuture(...)
: Event queue -> LaterFuture.delayed
: Event queue -> Laterawait Future.delayed(...)
: Event queue → Suspends, resumes as microtaskprint('8')
: Sync -> Immediately
So, microtasks always run before event queue tasks and that causes most unexpected behavior in Flutter apps.
Real-World Flutter Problem
Problem: UI Not Updating After await
void _onPressed() async {
setState(() {
_loading = true;
});
await Future.delayed(Duration(seconds: 2));
setState(() {
_loading = false;
});
}
You click a button. The spinner is supposed to show, wait 2 seconds, then disappear.
But… the UI doesn’t show the spinner during the delay.
Fix: Yield control using Future.delayed(Duration.zero)
void _onPressed() async {
setState(() => _loading = true);
await Future.delayed(Duration.zero); // give time for rebuild
await Future.delayed(Duration(seconds: 2));
setState(() => _loading = false);
}
Why does this work?
Because Future.delayed(Duration.zero)
moves the execution into the event queue, giving Flutter time to rebuild the UI before the delay.
How Dart Awaits Work (Under the Hood)
When you await
a Future
, Dart:
Registers a callback
Returns control to the event loop
When the
Future
completes, schedules that callback as a microtask
So, in an async
function, everything after await
is treated like a microtask.
Use Case: Background Sync in a Flutter App
Imagine you’re building a task manager app. After opening the home screen, it fetches user tasks from local storage, then syncs with the server:
void initState() {
super.initState();
_loadTasks();
}
void _loadTasks() async {
final local = await storage.getTasks();
setState(() => _tasks = local);
final updated = await api.syncTasks(local);
setState(() => _tasks = updated);
}
This seems fine… but users report UI Jank during startup.
Optimized Approach
void initState() {
super.initState();
// Let UI load first
WidgetsBinding.instance.addPostFrameCallback((_) => _loadTasks());
}
void _loadTasks() async {
final local = await storage.getTasks();
setState(() => _tasks = local);
// Let microtasks/UI catch up
await Future.delayed(Duration.zero);
final updated = await api.syncTasks(local);
setState(() => _tasks = updated);
}
addPostFrameCallback
: Defers work until after first frameFuture.delayed(Duration.zero)
: Gives the UI time to breathe
This respects Dart’s event loop while improving perceived performance.
Best Practices for Dart’s Event Loop
Do
Use
Future.delayed(Duration.zero)
to yield controlSchedule background work after first frame using
addPostFrameCallback
Use
scheduleMicrotask
for priority work (rare)Profile UI Jank with DevTools
Avoid
Heavy sync logic in UI methods
Calling async code in
initState()
without careMisusing
await
assuming it instantly pauses executionIgnoring long frame times during async ops
Conclusion
Dart’s event loop isn’t just an engine detail it shapes how your code behaves. Understanding it helps:
Write smoother UI updates
Debug async bugs
Build better architecture for real-world apps
The next time you await
doesn’t feel like it’s waiting, or your setState()
doesn’t fire as expected check the queues. It’s probably a microtask thing.
Bonus Tip: Want to Explore Live?
Paste this code into DartPad and tweak the order of Future
, scheduleMicrotask
, and print
statements to see the event loop in action.
Subscribe to my newsletter
Read articles from Md. Al - Amin directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Md. Al - Amin
Md. Al - Amin
Experienced Android Developer with a demonstrated history of working for the IT industry. Skilled in JAVA, Dart, Flutter, and Teamwork. Strong Application Development professional with a Bachelor's degree focused in Computer Science & Engineering from Daffodil International University-DIU.