Mastering Dart’s Event Loop: From Microtasks to Isolates
This article explains Dart’s single‑threaded event‑loop model, the distinction between microtask and event queues, how to schedule tasks, use Futures, async/await, isolates, and practical patterns like LoadBalancer and CancelableOperation to manage asynchronous operations in Flutter apps.
Dart Event Loop Model
In Flutter apps, asynchronous tasks such as network requests or file I/O are handled by a single‑threaded event loop rather than multiple OS threads. Dart maintains two queues: a microtask queue and an event queue. Tasks are processed FIFO, but the microtask queue has higher priority. After main() finishes, the event loop runs: it empties the microtask queue, then processes one event task, then checks the microtask queue again, and repeats until both queues are empty.
This model simplifies asynchronous programming because tasks are not pre‑empted, and exceptions only abort the current task without crashing the program.
Dart has two task queues: microtask and event.
The event loop starts after main() completes; microtasks are processed first.
Microtasks handle internal Dart work; events include Futures, timers, isolate messages, user input, I/O, and UI rendering.
Neither queue should run heavy‑weight computations to keep the UI responsive.
Exceptions abort only the current task; subsequent tasks continue.
Adding Tasks to Queues
1.1 Add to microtask queue
scheduleMicrotask(() => print('microtask1'));</code><code>Future.microtask(() => print('microtask2'));Using Future.microtask allows the result to be handled in a then callback.
1.2 Add to event queue
Future(() => print('event task'));Verifying the Event Loop
void main() {</code><code> print('main start');</code><code> Future(() => print('event task1'));</code><code> Future.microtask(() => print('microtask1'));</code><code> Future(() => print('event task1'));</code><code> Future.microtask(() => print('microtask2'));</code><code> print('main stop');</code><code>}Output:
main start</code><code>main stop</code><code>microtask1</code><code>microtask2</code><code>event task1</code><code>event task1The order follows the microtask‑first rule, not the source code order.
Asynchronous Implementation in Dart
Dart uses Future to represent asynchronous operations. Future objects can be created with several constructors:
Future() Future.delayed() Future.microtask() Future.sync()Future()
factory Future(FutureOr<T> computation()) {</code><code> _Future<T> result = new _Future<T>();</code><code> Timer.run(() {</code><code> try {</code><code> result._complete(computation());</code><code> } catch (e, s) {</code><code> _completeWithErrorCallback(result, e, s);</code><code> }</code><code> });</code><code> return result;</code><code>}It schedules the computation with a zero‑delay timer and catches exceptions.
Future.delayed()
factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {</code><code> _Future<T> result = new _Future<T>();</code><code> new Timer(duration, () {</code><code> if (computation == null) {</code><code> result._complete(null as T);</code><code> } else {</code><code> try {</code><code> result._complete(computation());</code><code> } catch (e, s) {</code><code> _completeWithErrorCallback(result, e, s);</code><code> }</code><code> }</code><code> });</code><code> return result;</code><code>}It adds the task after the specified delay.
Future.microtask()
factory Future.microtask(FutureOr<T> computation()) {</code><code> _Future<T> result = new _Future<T>();</code><code> scheduleMicrotask(() {</code><code> try {</code><code> result._complete(computation());</code><code> } catch (e, s) {</code><code> _completeWithErrorCallback(result, e, s);</code><code> }</code><code> });</code><code> return result;</code><code>}Places the task in the microtask queue, allowing then callbacks to handle the result.
Future.sync()
factory Future.sync(FutureOr<T> computation()) {</code><code> try {</code><code> var result = computation();</code><code> if (result is Future<T>) {</code><code> return result;</code><code> } else {</code><code> return new _Future<T>.value(result as dynamic);</code><code> }</code><code> } catch (error, stackTrace) {</code><code> var future = new _Future<T>();</code><code> AsyncError? replacement = Zone.current.errorCallback(error, stackTrace);</code><code> if (replacement != null) {</code><code> future._asyncCompleteError(replacement.error, replacement.stackTrace);</code><code> } else {</code><code> future._asyncCompleteError(error, stackTrace);</code><code> }</code><code> return future;</code><code> }</code><code>}The computation runs immediately without being placed in any queue.
then, catchError, whenComplete
Future(() {</code><code> throw 'error';</code><code>}).then((_) {</code><code> print('success');</code><code>}).catchError((error) {</code><code> print(error);</code><code>}).whenComplete(() {</code><code> print('completed');</code><code>});Output:
error</code><code>completed catchErrorhandles exceptions; whenComplete runs regardless of success.
async / await
void main() async {</code><code> print(await getData());</code><code>}</code><code>Future<int> getData() async {</code><code> final a = await Future.delayed(Duration(seconds: 1), () => 1);</code><code> final b = await Future.delayed(Duration(seconds: 1), () => 1);</code><code> return a + b;</code><code>} asyncfunctions return a Future; await pauses execution until the awaited future completes, making asynchronous code look synchronous. Exceptions should be caught with try/catch.
Isolate Introduction
Heavy tasks should not run on the main isolate because they block UI rendering. An isolate runs in its own OS thread, providing true parallelism. Flutter offers APIs to spawn isolates.
compute
void main() async {</code><code> compute<String, String>(</code><code> getData,</code><code> 'Alex',</code><code> ).then((result) {</code><code> print(result);</code><code> });</code><code>}</code><code>String getData(String name) {</code><code> // simulate a 3‑second workload</code><code> sleep(Duration(seconds: 3));</code><code> return 'Hello $name';</code><code>} computeruns the function in a new isolate, but creating many isolates can be costly in memory and time.
LoadBalancer (Isolate Pool)
// Create a LoadBalancer with 2 isolates</code><code>Future<LoadBalancer> loadBalancerCreator = LoadBalancer.create(2, IsolateRunner.spawn);</code><code>late LoadBalancer loadBalancer;</code><code>void main() async {</code><code> loadBalancer = await loadBalancerCreator;</code><code> final result = await loadBalancer.run<String, String>(getData, 'Alex');</code><code> print(result);</code><code>}</code><code>String getData(String name) {</code><code> sleep(Duration(seconds: 3));</code><code> return 'Hello $name';</code><code>}The LoadBalancer creates an isolate pool and balances tasks across them, avoiding the overhead of repeatedly spawning isolates.
Practical Tips
Controlling Task Order
When tasks depend on previous results, chaining then calls or using async/await improves readability.
void main() async {</code><code> print(await getData());</code><code>}</code><code>Future<int> getData() {</code><code> int value = 0;</code><code> return Future(() => 1).then((r1) {</code><code> value += r1;</code><code> return Future(() => 2);</code><code> }).then((r2) {</code><code> value += r2;</code><code> return Future(() => 3);</code><code> }).then((r3) {</code><code> value += r3;</code><code> return value;</code><code> });</code><code>}This eliminates “callback hell” and makes the flow clearer.
Using async/await
void main() async {</code><code> print(await getData());</code><code>}</code><code>Future<int> getData() async {</code><code> int value = 0;</code><code> value += await Future(() => 1);</code><code> value += await Future(() => 2);</code><code> value += await Future(() => 3);</code><code> return value;</code><code>}The async/await version is even more concise.
Cancelling Tasks
Since Dart futures cannot be interrupted, cancellation is simulated by ignoring callbacks after the task finishes.
CancelableOperation
void main() async {</code><code> final cancelableOperation = CancelableOperation.fromFuture(</code><code> Future(async {</code><code> print('start');</code><code> await Future.delayed(Duration(seconds: 3));</code><code> print('end');</code><code> }),</code><code> onCancel: () => print('cancel...'),</code><code> );</code><code> cancelableOperation.value.then((val) {</code><code> print('finished');</code><code> });</code><code> Future.delayed(Duration(seconds: 1)).then((_) => cancelableOperation.cancel());</code><code>}The operation runs its future unless it is cancelled before completion.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
JD Cloud Developers
JD Cloud Developers (Developer of JD Technology) is a JD Technology Group platform offering technical sharing and communication for AI, cloud computing, IoT and related developers. It publishes JD product technical information, industry content, and tech event news. Embrace technology and partner with developers to envision the future.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
