| |

Dart 38 ๐ŸŽฏ Async/Await Mechanics, Microtasks, and Event Queue Scheduling

async and await are the surface syntax that most Dart developers use every day. An async function returns a Future. An await expression pauses the function until the future completes, then resumes with the value. The code reads like synchronous code, and the compiler transforms it into the callback machinery of then and catchError. The transformation is not a metaphor โ€” it is a mechanical rewrite. Understanding the rewrite is what makes the behavior predictable.

The key insight is that await does not block the thread. When an async function reaches an await, it suspends. The function’s state โ€” its local variables, its position in the code โ€” is captured into a heap-allocated structure. The thread is released to process other work. When the future being awaited completes, the rest of the function is scheduled as a microtask and resumed from the suspension point. This is why await is safe in a UI framework: the thread is never blocked, only the function is suspended.

The scheduling detail matters enormously. The code after an await runs as a microtask, not as an event. This means it runs after the current synchronous code finishes, but before any pending events. Two await expressions in sequence do not run their continuations in the same microtask โ€” each await creates a separate suspension and a separate microtask. And because await uses microtasks, a chain of awaits can starve the event queue if the futures complete synchronously. These behaviors are not obvious from the syntax, and they are the source of most async ordering surprises.

This chapter covers three areas. First, why async/await exists as syntax sugar โ€” the problem of callback chains and how the compiler rewrites async/await into then calls. Second, how await suspends and resumes โ€” the state machine, the capture of local variables, and the scheduling of the continuation as a microtask. Third, how async/await interacts with the event and microtask queues โ€” the ordering rules, the difference between await Future.value() and await Future(), and the ways a chain of awaits can starve the event queue. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the suspension, the microtask scheduling, and the queue interaction.

Key point: An async function returns a Future. An await expression suspends the function, releases the thread, and schedules the continuation as a microtask when the awaited future completes. The code after an await runs as a microtask, not as an event, which determines its ordering relative to synchronous code, other microtasks, and events .


Why async/await exists

The callback pyramid problem. A sequence of asynchronous operations written with then becomes a pyramid of nested callbacks. Each step is a then, and each then is indented one level deeper. Error handling compounds the problem: a single catchError at the end catches errors from every step, but a catchError in the middle changes the flow in ways that are hard to see. The code is technically correct, but its structure no longer matches the sequence of operations it performs. async/await restores the structure. Each step is a line of code, and the sequence reads top to bottom, like synchronous code .

The state-machine problem. A synchronous function has a call stack frame: its local variables and its current position. When it calls another function, the frame is pushed onto the stack, and when the call returns, the frame is restored. An asynchronous function cannot keep its frame on the stack while it waits, because the stack must be unwound to let the thread do other work. The function’s state must be moved to the heap. The compiler transforms an async function into a state machine: each await is a state, and the local variables become fields of the state object. When the future completes, the state machine resumes at the correct state with the correct local values. This transformation is what makes await possible without blocking the thread .

The readability problem. The callback API is composable and precise, but it is not readable for sequences. A then chain that fetches a user, then their posts, then the comments on each post, is a pipeline whose data flow is implicit in the return values. With await, the same sequence is three lines, each assigning to a variable. The variables make the data flow explicit, and the error handling is a try/catch block that matches the synchronous form. The compiler produces the same then and catchError machinery either way. The syntax is sugar, but the sugar changes which parts of the code the reader must hold in their head .

The error-handling problem. With then/catchError, an error propagates through the chain and is caught by the first catchError that handles it. This is correct but indirect: the reader must scan the chain to find the handler. With async/await, a try/catch block wraps the awaits, and the error handling is where the reader expects it. The compiler rewrites the try/catch into a catchError on the future returned by the async function. The behavior is identical, but the source is easier to reason about .

The debugger problem. Callback chains are difficult to debug because the call stack is not preserved across asynchronous boundaries. When a then callback runs, the stack does not show the code that attached it. async/await improves this: Dart’s debugger can reconstruct the asynchronous call stack across await points, showing a logical stack that includes the suspended frames. This is not a property of the sugar itself but of the runtime support that async/await enables. The state machine carries enough information for the debugger to present a coherent stack .

The trade-off. async/await makes the function asynchronous. Every caller must handle the returned future, either by awaiting it or by attaching callbacks. An async function cannot be called from synchronous code without bridging the gap. It also hides the scheduling: the await looks like a pause, but it is a suspension that schedules a microtask. Code that assumes the continuation runs immediately after the awaited future completes can be wrong about the ordering. The trade-off is between the readability of sequential syntax and the explicitness of the scheduling that the syntax conceals.


a. The async transformation

An async function returns a Future. If the function body returns a value, the future completes with that value. If the body throws, the future completes with that error. The compiler wraps the body in the future machinery automatically .

Future<int> fetchCount() async {
  return 42;
}

This is equivalent to:

Future<int> fetchCount() {
  return Future.value(42);
}

The async keyword changes the function’s return type from int to Future<int> and wraps the return value in a future. If the function returns nothing, the return type is Future<void>.

An async function starts executing synchronously when called, up to the first await. The code before the first await runs immediately, in the same synchronous execution as the caller. The first await is where the suspension begins.

Future<void> example() async {
  print('A'); // runs synchronously
  await Future.value(1);
  print('B'); // runs as a microtask
}

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

Output:

start
A
end
B

The print('A') runs synchronously because it is before the first await. The print('B') runs after the synchronous code completes, because the continuation after await is scheduled as a microtask. The example() call does not block; it returns a future immediately, and print('end') runs before print('B') .

This is the most important behavior of async functions: the code before the first await runs synchronously, and the code after each await runs as a microtask. An async function without an await runs entirely synchronously, but still returns a future.

b. How await suspends and resumes

An await expression evaluates the future, suspends the function, and registers a continuation. When the future completes, the continuation is scheduled as a microtask. The function resumes with the value (or error) from the future.

The suspension captures the function’s state: the local variables, the position in the code, and the exception handlers. This state is stored in a heap-allocated object generated by the compiler. The thread is released and continues processing other work. There is no stack frame for the suspended function on the call stack โ€” the state object replaces it .

Future<void> example() async {
  final a = 1;
  final b = await fetchValue();
  print(a + b);
}

When await fetchValue() is reached, the function suspends. The local a and the position in the code are saved. When fetchValue() completes, the continuation is scheduled as a microtask, and the function resumes: b is assigned the value, and print(a + b) runs. The local a is still available because it was captured in the state object.

The continuation is a microtask. This means it runs after the current synchronous code finishes, but before any pending events. If multiple awaits are chained, each continuation is a separate microtask:

Future<void> example() async {
  print('1');
  await Future.value(1);
  print('2');
  await Future.value(2);
  print('3');
}

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

Output:

1
main
2
3

The print('1') runs synchronously. The first await suspends and schedules the continuation as a microtask. The print('main') runs synchronously. Then the microtask queue drains: print('2') runs, the second await suspends and schedules another microtask, and print('3') runs. The two continuations are separate microtasks, but they drain in the same microtask pass because the microtask queue is not interrupted between them.

An await on an already-completed future still suspends. await Future.value(1) does not skip the suspension; it schedules the continuation as a microtask even though the value is already available:

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

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

Output:

before
main
after

The await suspends, the continuation is scheduled as a microtask, and print('main') runs first. The print('after') runs when the microtask queue drains. This is the same behavior as Future.value(1).then((_) => print('after')), because await is rewritten to a then call.

c. Async/await and the event queue

The continuation after an await is a microtask, not an event. This determines its ordering relative to other queues:

  • Synchronous code runs first.
  • The microtask queue drains completely.
  • The next event is processed.
  • The microtask queue drains again.
  • Repeat.

Because await continuations are microtasks, they run before events. A Future() callback, a timer, or an I/O completion is an event, and it waits for the microtask queue to be empty. This means an async function that awaits a future which completes with an event will resume as a microtask after that event โ€” but any other microtasks scheduled before it will run first.

The distinction between await Future.value(1) and await Future(() => 1) is the distinction between a microtask and an event:

Future<void> example() async {
  await Future.value(1);          // microtask
  print('after value');

  await Future(() => 1);          // event
  print('after event');
}

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

Output:

sync
after value
microtask
event
after event

The await Future.value(1) suspends and schedules its continuation as a microtask. The await Future(() => 1) suspends and waits for an event. The synchronous code (sync) runs, the microtask queue drains (after value, then microtask), then the event queue processes the Future(() => 1) callback (event), which completes the awaited future, which schedules the continuation as a microtask (after event). The continuation of the second await runs after the event that completed it, as a new microtask.

A chain of awaits on synchronously-completed futures can starve the event queue. Each continuation is a microtask, and if each microtask schedules another microtask, the event queue never gets a turn:

Future<void> loop() async {
  for (var i = 0; i < 3; i++) {
    await Future.value(i);
    print('iteration $i');
  }
}

void main() {
  loop();
  Future(() => print('event'));
  print('sync');
}

Output:

sync
iteration 0
iteration 1
iteration 2
event

The Future.value continuations are microtasks. The loop’s continuations are scheduled back-to-back, and the event queue waits until the microtask queue is empty. The event prints last, after all iterations complete. If the loop were infinite, the event would never print โ€” this is microtask starvation.


Complete Example Session

// ============================================
// PART 1: THE ASYNC TRANSFORMATION
// ============================================

// An async function returns a Future.

Future<int> fetchCount() async {
  return 42;
}

// Equivalent to:
// Future<int> fetchCount() {
//   return Future.value(42);
// }


// ============================================
// PART 2: SYNCHRONOUS UNTIL THE FIRST AWAIT
// ============================================

Future<void> example() async {
  print('A'); // synchronous
  await Future.value(1);
  print('B'); // microtask
}

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

// Output: start, A, end, B
// The code before the first await runs
// synchronously. The code after runs as a
// microtask.


// ============================================
// PART 3: AWAIT SUSPENDS AND RESUMES
// ============================================

Future<int> fetchValue() async => 10;

Future<void> withLocal() async {
  final a = 1;
  final b = await fetchValue();
  print(a + b); // 11
}

// When await is reached, the function suspends.
// The local 'a' is captured in the state object.
// When the future completes, the continuation is
// scheduled as a microtask, and 'a' is restored.


// ============================================
// PART 4: MULTIPLE AWAITS, MULTIPLE MICROTASKS
// ============================================

Future<void> twoAwaits() async {
  print('1');
  await Future.value(1);
  print('2');
  await Future.value(2);
  print('3');
}

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

// Output: 1, main, 2, 3
// Each await schedules a separate microtask.
// The microtask queue drains without interruption.


// ============================================
// PART 5: AWAIT ON A COMPLETED FUTURE STILL SUSPENDS
// ============================================

Future<void> completed() async {
  print('before');
  await Future.value(1);
  print('after');
}

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

// Output: before, main, after
// Even though Future.value(1) is already complete,
// the await suspends and schedules a microtask.


// ============================================
// PART 6: AWAIT FUTURE.VALUE VS AWAIT FUTURE()
// ============================================

Future<void> comparison() async {
  await Future.value(1);
  print('after value');

  await Future(() => 1);
  print('after event');
}

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

// Output:
// sync
// after value
// microtask
// event
// after event
//
// Future.value is a microtask. Future() is an event.
// The continuation of the second await runs after
// the event that completed it.


// ============================================
// PART 7: ERROR HANDLING WITH TRY/CATCH
// ============================================

Future<int> failing() async {
  throw Exception('boom');
}

Future<void> handleError() async {
  try {
    final value = await failing();
    print('value: $value');
  } catch (e) {
    print('caught: $e');
  } finally {
    print('done');
  }
}

// Output: caught: Exception: boom, done
// The compiler rewrites try/catch into
// catchError on the returned future.


// ============================================
// PART 8: AWAIT IN A LOOP โ€” MICROTASK STARVATION
// ============================================

Future<void> loop() async {
  for (var i = 0; i < 3; i++) {
    await Future.value(i);
    print('iteration $i');
  }
}

void main() {
  loop();
  Future(() => print('event'));
  print('sync');
}

// Output:
// sync
// iteration 0
// iteration 1
// iteration 2
// event
//
// Each iteration schedules a microtask. The event
// waits until the microtask queue is empty.


// ============================================
// PART 9: THE EQUIVALENT CALLBACK CHAIN
// ============================================

// The compiler rewrites:
//
//   final value = await fetchValue();
//   print(value);
//
// into:
//
//   fetchValue().then((value) {
//     print(value);
//   });
//
// The try/catch is rewritten into catchError.
// The await is rewritten into then.


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

Future<void> ordering() async {
  print('async start');           // synchronous
  await Future.value('value');    // microtask continuation
  print('after value');           // microtask
  await Future(() => 'event');    // event continuation
  print('after event');           // microtask
}

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

// Output:
// async start
// sync
// after value
// microtask
// event
// after event
//
// Order:
//   1. Synchronous: async start, sync
//   2. Microtasks: after value, microtask
//   3. Events: event
//   4. Microtasks: after event

The ten parts show the async transformation, synchronous execution before the first await, suspension and local capture, multiple awaits, await on completed futures, the microtask vs event distinction, error handling, microtask starvation, the callback equivalent, and the complete ordering.


Quick Reference

The Async Transformation

SourceEquivalent
Future<int> f() async => 42;Future<int> f() => Future.value(42);
await fetchValue();fetchValue().then((value) => ...);
try { await f(); } catch (e) { }f().catchError((e) => ...);
finally { }.whenComplete(() => ...)

Execution Order

PhaseWhat runs
SynchronousCode before the first await
MicrotaskContinuation after each await
EventCallbacks from Future(), timers, I/O
MicrotaskContinuations scheduled by events

await Scheduling

ExpressionContinuation scheduled as
await Future.value(1)Microtask
await Future(() => 1)Microtask (after the event completes)
await fetchData()Microtask (after the I/O event completes)
await someFutureMicrotask

Microtask Starvation

PatternEffect
Loop with await Future.value()Microtasks drain before events
Infinite loop of awaitsEvents never run
Long synchronous workNo microtasks, no events

Best Practices

โœ… Do This:

// Use await for sequential operations
final user = await fetchUser();
final posts = await fetchPosts(user.id);                               // โœ…
// Use try/catch/finally for error handling
try { await risky(); } catch (e) { } finally { }                       // โœ…
// Understand that code after await is a microtask
await Future.value(1); print('microtask');                             // โœ…
// Yield to the event loop in long loops
await Future(() {});                                                   // โœ…
// Use Future.wait for parallel operations
final results = await Future.wait([fetchA(), fetchB()]);               // โœ…

โŒ Don’t Do This:

// Don't assume code after await runs synchronously
await Future.value(1);
print('after'); // microtask, not sync                                // โŒ
// Don't create infinite await loops
while (true) { await Future.value(0); }                               // โŒ (starves events)
// Don't mix then and await confusingly
await fetch().then((v) => print(v));                                  // โŒ (redundant)
// Don't forget to await
fetchUser(); print(user); // user is a Future                          // โŒ
// Don't use await in a loop for independent operations
for (final id in ids) { await fetch(id); }                            // โŒ (use Future.wait)

Common Pitfalls

PitfallWhy It HappensFix
Code after await runs too lateAssuming synchronous continuationIt is a microtask
Event delayed by await loopMicrotask starvationUse Future() to yield to events
Local variables lost after awaitNot understanding state captureThe state machine captures them
Unhandled async errorNo try/catch or catchErrorAdd error handling
await on non-Futureawait accepts any valueNon-futures complete immediately as microtasks
then inside asyncMixing two stylesUse await consistently
Parallel operations serializedawait in a loopUse Future.wait

Real-World Examples

1. Sequential Awaits

final user = await fetchUser();
final posts = await fetchPosts(user.id);

2. Parallel Awaits

final [user, posts] = await Future.wait([fetchUser(), fetchPosts()]);

3. try/catch/finally

try { await fetch(); } catch (e) { } finally { }

4. Code Before First Await

Future<void> f() async { print('sync'); await g(); print('microtask'); }

5. Await on Completed Future

await Future.value(1); // still schedules a microtask

6. Yield to Event Loop

await Future(() {}); // schedules an event

7. Await in a Loop

for (final id in ids) { await process(id); }

8. The Callback Equivalent

fetch().then((v) => print(v));

9. Multiple Awaits

await a(); await b(); // two separate microtasks

10. Error Propagation

await failing(); // throws in the enclosing async function

Visual

Suspension and Resumption

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SUSPENSION AND RESUMPTION                                   โ”‚
โ”‚                                                              โ”‚
โ”‚  async function:                                             โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  print('A');              โ† runs synchronously       โ”‚    โ”‚
โ”‚  โ”‚  final b = await f();     โ† SUSPENDS here            โ”‚    โ”‚
โ”‚  โ”‚  print(b);                โ† runs as microtask        โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  When await is reached:                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  1. State saved to heap: local vars, position        โ”‚    โ”‚
โ”‚  โ”‚  2. Thread released                                 โ”‚    โ”‚
โ”‚  โ”‚  3. Function returns a pending Future               โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  When f() completes:                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  1. Continuation scheduled as microtask              โ”‚    โ”‚
โ”‚  โ”‚  2. State restored from heap                        โ”‚    โ”‚
โ”‚  โ”‚  3. Execution resumes after await                   โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Queue Ordering

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  QUEUE ORDERING WITH ASYNC/AWAIT                             โ”‚
โ”‚                                                              โ”‚
โ”‚  Future<void> ordering() async {                             โ”‚
โ”‚    print('async start');      โ† sync                         โ”‚
โ”‚    await Future.value('v');   โ† microtask continuation       โ”‚
โ”‚    print('after value');      โ† microtask                    โ”‚
โ”‚    await Future(() => 'e');   โ† event continuation           โ”‚
โ”‚    print('after event');      โ† microtask                    โ”‚
โ”‚  }                                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  main() {                                                    โ”‚
โ”‚    ordering();                                               โ”‚
โ”‚    scheduleMicrotask(() => print('microtask'));              โ”‚
โ”‚    Future(() => print('event'));                             โ”‚
โ”‚    print('sync');                                            โ”‚
โ”‚  }                                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  1. Sync:        async start, sync                   โ”‚    โ”‚
โ”‚  โ”‚  2. Microtasks:  after value, microtask              โ”‚    โ”‚
โ”‚  โ”‚  3. Events:      event                               โ”‚    โ”‚
โ”‚  โ”‚  4. Microtasks:  after event                         โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  Continuations are microtasks. Events wait for the           โ”‚
โ”‚  microtask queue to empty.                                   โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Microtask Starvation

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  MICROTASK STARVATION                                        โ”‚
โ”‚                                                              โ”‚
โ”‚  Future<void> loop() async {                                 โ”‚
โ”‚    for (var i = 0; i < 3; i++) {                             โ”‚
โ”‚      await Future.value(i);   โ† microtask                    โ”‚
โ”‚      print('iteration $i');                                  โ”‚
โ”‚    }                                                         โ”‚
โ”‚  }                                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  main() {                                                    โ”‚
โ”‚    loop();                                                   โ”‚
โ”‚    Future(() => print('event'));                             โ”‚
โ”‚    print('sync');                                            โ”‚
โ”‚  }                                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  Microtask queue:                                    โ”‚    โ”‚
โ”‚  โ”‚  [iteration 0] โ†’ [iteration 1] โ†’ [iteration 2]       โ”‚    โ”‚
โ”‚  โ”‚                                                       โ”‚    โ”‚
โ”‚  โ”‚  Event queue:                                        โ”‚    โ”‚
โ”‚  โ”‚  [event] โ† waits until microtasks are empty          โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  Output: sync, iteration 0, iteration 1, iteration 2, event  โ”‚
โ”‚                                                              โ”‚
โ”‚  An infinite loop of awaits would starve the event queue     โ”‚
โ”‚  entirely. Events would never run.                           โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Callback Equivalent

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ASYNC/AWAIT vs CALLBACKS                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  async/await:                                                โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  Future<void> f() async {                            โ”‚    โ”‚
โ”‚  โ”‚    try {                                             โ”‚    โ”‚
โ”‚  โ”‚      final v = await g();                            โ”‚    โ”‚
โ”‚  โ”‚      print(v);                                       โ”‚    โ”‚
โ”‚  โ”‚    } catch (e) {                                     โ”‚    โ”‚
โ”‚  โ”‚      print(e);                                       โ”‚    โ”‚
โ”‚  โ”‚    } finally {                                       โ”‚    โ”‚
โ”‚  โ”‚      print('done');                                  โ”‚    โ”‚
โ”‚  โ”‚    }                                                 โ”‚    โ”‚
โ”‚  โ”‚  }                                                   โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  Compiler rewrite:                                           โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  Future<void> f() {                                  โ”‚    โ”‚
โ”‚  โ”‚    return g()                                        โ”‚    โ”‚
โ”‚  โ”‚      .then((v) => print(v))                          โ”‚    โ”‚
โ”‚  โ”‚      .catchError((e) => print(e))                    โ”‚    โ”‚
โ”‚  โ”‚      .whenComplete(() => print('done'));             โ”‚    โ”‚
โ”‚  โ”‚  }                                                   โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  await    โ†’ then                                             โ”‚
โ”‚  catch    โ†’ catchError                                       โ”‚
โ”‚  finally  โ†’ whenComplete                                     โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
async functionReturns a Future; body is a state machine
Before first awaitRuns synchronously
After each awaitRuns as a microtask
SuspensionState saved to heap; thread released
ResumptionContinuation scheduled as a microtask
await Future.value()Continuation is a microtask
await Future()Continuation is a microtask after the event
try/catchRewritten to catchError on the returned future
finallyRewritten to whenComplete
Microtask starvationAwait loops can delay or block events

Key takeaways:

  • async/await is syntax sugar for then/catchError/whenComplete. The compiler transforms an async function into a state machine that returns a future. await becomes then, catch becomes catchError, and finally becomes whenComplete .
  • Code before the first await runs synchronously. The function starts executing immediately when called. The suspension begins at the first await, not at the function’s start.
  • The continuation after an await is a microtask. It runs after the current synchronous code, before any pending events. This is why await Future.value(1) does not run the continuation immediately.
  • Each await is a separate suspension and a separate microtask. Two awaits in sequence produce two microtasks, not one. The microtask queue drains between them without processing events.
  • await Future.value() and await Future() differ in scheduling. Future.value completes as a microtask; Future() completes as an event. The continuation of the await runs after the future completes, so the event case waits for the event queue.
  • The state machine captures local variables. When an async function suspends, its local variables and position are saved to a heap-allocated object. When it resumes, the state is restored. This is why locals remain available after an await.
  • Await loops can starve the event queue. A loop that awaits synchronously-completed futures schedules a microtask per iteration. The event queue waits until the microtask queue is empty, so a long loop delays or blocks events.
  • async/await improves readability and debuggability. The syntax matches the sequence of operations, and the debugger can reconstruct the asynchronous call stack across await points. The callback API is still available for cases where the surrounding function cannot be async.

Remember: async/await is not a separate concurrency model โ€” it is a rewrite of the callback API into sequential syntax. The async keyword makes a function return a future. The await keyword suspends the function, releases the thread, and schedules the continuation as a microtask. The code before the first await runs synchronously, and the code after each await runs as a microtask. This scheduling is what determines the order of execution relative to other microtasks and events. Understanding the rewrite โ€” await as then, catch as catchError, finally as whenComplete โ€” makes the behavior predictable and the ordering explainable. The syntax is sugar, but the machinery underneath is the same event loop and the same two queues.



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!