Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Dart (3.13)  Module 11 – Asynchronous programming

Dart's event loop, microtasks and futures

How Dart runs async code on one thread: the event and microtask queues, futures chained with then, catchError and whenComplete, and Completers.

  • Intermediate
  • 15 minutes
  • Examples run with dart 3.13.5
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain how Dart's event loop takes work from its microtask and event queues
  • Predict the order in which synchronous code, microtasks and timer callbacks run
  • Create and chain futures with then, catchError and whenComplete
  • Wrap a callback-based API in a future with a Completer
  • Recognise synchronous work that holds up every timer and callback

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how Dart's event loop takes work from its microtask and event queues
  • Predict the order in which synchronous code, microtasks and timer callbacks run
  • Create and chain futures with then, catchError and whenComplete
  • Wrap a callback-based API in a future with a Completer
  • Recognise synchronous work that holds up every timer and callback

Before you start

On this page

A Dart program starts a timer that is due in 10 ms, then spends 300 ms adding numbers in a loop. The timer runs only after the loop has ended, at least 290 ms late, because the program has one thread and the timer’s callback must wait its turn.

Everything asynchronous in Dart, from timers and file reads to futures, works by that rule. This lesson shows the loop that hands out the turns and its two queues, with programs that ran on Dart 3.13.5. You will predict the order in which callbacks run, chain futures, handle their errors, adapt a callback API with a Completer and spot the work that holds every other callback up.

One isolate runs one piece of code at a time

A timer with a delay of zero sounds as if it runs at once. This program starts one and then loops 20 million times:

zero_timer.dart: a zero-duration timer waits for main Dart · zero_timer.dart
// A timer with a zero duration still waits for main to finish.
import 'dart:async';

void main() {
  Timer(Duration.zero, () => print('2. the zero-duration timer runs'));
  var sum = 0;
  for (var i = 0; i < 20000000; i++) {
    sum += i;
  }
  print('1. main has finished its loop, sum $sum');
}

Output

1. main has finished its loop, sum 199999990000000
2. the zero-duration timer runs

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run zero_timer.dart

Line 2 came after line 1, although the timer was due before the loop had even started. A Dart program runs in an isolate: An independent unit of execution in Dart with its own memory and one thread that runs an event loop. Isolates share no mutable objects and cooperate by sending messages. More in “Isolates and Isolate.run for heavy work”, and each isolate has one thread that runs one piece of code at a time. What decides the next piece is the event loop: The loop inside each Dart isolate that runs one piece of code at a time: main first, then each waiting microtask, then the next event, each to its end, until nothing is left to wait for.: it runs main to its end, then takes the next waiting callback, runs it to its end, and repeats until nothing is left to wait for. The Dart concurrency page describes it as a loop that processes events one at a time, in the order they were queued.

A callback is never interrupted halfway. The timer’s callback was ready early, but it had to wait until main gave the thread back. That is the whole model: no callback runs in parallel with another one in the same isolate, so two callbacks never see each other’s half-finished work.

Checkpoint

Check: One isolate, one piece of code at a time

  1. One question main starts Timer(Duration.zero, f) and then runs a loop for 2 seconds. When does f run?

    Choose one answer.

    Show the answer: One question

    Answer: After main has finished, so about 2 seconds later

    zero_timer.dart printed main's line first. A zero delay only means the callback is ready at once; it still waits for its turn.

Microtasks run before the next event

The loop keeps two queues. The event queue: The queue of outside work an isolate's event loop takes one item at a time: timer callbacks, finished I/O and messages from other isolates, in the order they arrived. holds timer callbacks, finished I/O and messages from other isolates. The microtask: A short follow-up job, such as a then callback of a completed future or a scheduleMicrotask call. The event loop runs every waiting microtask, in order, before it takes the next event. queue holds short follow-up jobs, such as the callback of a future that has already completed. The rule between them: whenever the running code ends, main or an event’s callback, the loop runs every waiting microtask, in order, before it takes the next event.

This program schedules work in both queues and numbers each print in the order it appears:

async_order.dart: eight prints in the order they ran Dart · async_order.dart
// Eight prints, numbered in the order they appear. Each line says which queue its callback waited in.
import 'dart:async';

Future<void> main() async {
  print('1 main starts');
  Timer.run(() => print('6 Timer.run: an event'));
  Future(() => print('7 Future(() ...): an event'));
  scheduleMicrotask(() => print('4 scheduleMicrotask: a microtask'));
  Future.value('ready').then((_) => print('5 then on a completed future: a microtask'));
  Future.sync(() => print('2 Future.sync: runs its callback now'));
  print('3 main reaches its await');
  await Future.pause(Duration.zero);
  print('8 after await Future.pause(Duration.zero)');
}

Output

1 main starts
2 Future.sync: runs its callback now
3 main reaches its await
4 scheduleMicrotask: a microtask
5 then on a completed future: a microtask
6 Timer.run: an event
7 Future(() ...): an event
8 after await Future.pause(Duration.zero)

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run async_order.dart

Future.sync ran its function at once, inside main, so 2 came before 3. The two microtasks came next, 4 and 5, in the order they were scheduled. Only then did the loop take the events, 6 and 7, also in their order. The await in main waited for a third timer, so 8 came last. Step through the same run and watch both queues:

Interactive · step by step The eight prints of async_order.dart, queue by queue Step through async_order.dart. Watch each callback join a queue, and the loop empty the microtask queue first.
The eight prints of async_order.dart, queue by queue, step 11 of 11. Showing running now (main resumes), printed so far (1 to 8), microtask queue (empty), event queue (empty). In focus: running now.firstthenrunning nowmain resumesprinted so far1 to 8microtask queueemptyevent queueempty

The drawing shows the whole scene; its 11 steps are listed below.

Every step as text
  1. main starts. The loop's first job is main itself. It runs from its first line, and nothing else can run on this isolate until main stops. It prints 1.
  2. Timer.run adds an event. Timer.run does not run its callback now. It starts a timer with a zero delay, and the callback waits in the event queue.
  3. Future() adds another event. Future(() ...) also schedules its function with a timer, so it waits in the event queue too, behind Timer.run.
  4. scheduleMicrotask. scheduleMicrotask puts its callback in the microtask queue, which the loop empties before it takes any event.
  5. then on a completed future. Future.value('ready') is already complete, so then schedules its callback as a microtask, behind the one already waiting.
  6. Future.sync runs at once. Future.sync calls its function now, inside main, and wraps the result in a future. That is why 2 is printed before any queue is touched.
  7. main reaches its await. main prints 3. Future.pause(Duration.zero) starts a timer, a third event, and await hands control back to the loop until that timer fires.
  8. Microtasks first. main's synchronous part has ended. Before it looks at the events, the loop empties the microtask queue in order, 4 and then 5.
  9. Then the oldest event. With no microtask waiting, the loop takes the oldest event, the Timer.run callback, which prints 6. After each event it checks the microtask queue again.
  10. The next event. The microtask queue is still empty, so the loop takes the next event, the function given to Future(), which prints 7.
  11. The pause ends and main resumes. The pause timer fires and completes its future. That schedules the rest of main as a microtask, which runs at once and prints 8. Every queue is now empty.

The scheduleMicrotask documentation states the guarantee in so many words: its callbacks always run in order and before other asynchronous events such as timers. Future.microtask(...) uses the same queue, while Timer.run, Future(...) and Future.delayed(...) all wait in the event queue.

Key idea

Whenever the running code ends, the loop empties the microtask queue in order before it takes the next event, so a microtask always runs before any timer that is already waiting.

Checkpoint

Check: Microtasks before events

  1. Question 1 of 2 A program calls Timer.run(a), then scheduleMicrotask(b), then prints c. In what order do a, b and c run?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: c, b, a

    Synchronous code first, then every waiting microtask, then the events in their order.

  2. Question 2 of 2 Which of these wait in the microtask queue?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • A then callback on a future that has already completed
    • A function given to scheduleMicrotask
    • A function given to Future.microtask

    The microtask queue holds short follow-up jobs; timers, I/O and isolate messages are events.

A future is a value that arrives later

A future: An object of type Future<T> that completes once, later, with a value of type T or with an error. Callbacks registered with then, catchError and whenComplete, or an await, receive the result. is an object that will complete once, either with a value or with an error. Code that needs the value registers a callback with then, and then returns a new future for the callback’s own result, so steps can be chained. This program looks up two prices, and the second item does not exist:

price_chain.dart: then, catchError and whenComplete Dart · price_chain.dart
// Two price lookups chained with then, catchError and whenComplete. The second sku does not exist.
import 'dart:async';

class UnknownSkuException implements Exception {
  UnknownSkuException(this.sku);
  final String sku;
  @override
  String toString() => 'UnknownSkuException: no price for "$sku"';
}

const prices = {'pen': 1200, 'pad': 4500};

Future<int> fetchPrice(String sku) => Future.delayed(const Duration(milliseconds: 20), () {
      final price = prices[sku];
      if (price == null) throw UnknownSkuException(sku);
      return price;
    });

void lookUp(String sku) {
  print('$sku: asking');
  fetchPrice(sku)
      .then((price) => price * 90 ~/ 100)
      .then((price) => print('$sku: $price after 10 % off'))
      .catchError((Object e) => print('$sku: lookup failed, $e'))
      .whenComplete(() => print('$sku: done'));
}

void main() {
  lookUp('pen');
  lookUp('ink');
  print('main: both lookups started');
}

Output

pen: asking
ink: asking
main: both lookups started
pen: 1080 after 10 % off
pen: done
ink: lookup failed, UnknownSkuException: no price for "ink"
ink: done

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run price_chain.dart

Both lookups started before either finished: main: both lookups started came third, and the prices arrived 20 ms later, after main had ended. For pen the value went through both then steps: 1200, then 1080 after the discount. For ink, fetchPrice threw inside its delayed callback, so its future completed with an error. Both then steps were skipped, catchError received the UnknownSkuException, and whenComplete ran on both paths, the way finally runs after a try.

The ways to make a future differ in when their function runs and which queue it waits in:

Constructor Its function runs Completes
Future.sync(f) now, inside the caller with f’s value or error
Future.value(v), Future.syncValue(v) (no function) with v; a then callback waits for a microtask
Future.microtask(f) from the microtask queue when f has run
Future(f), Timer.run(f) from the event queue when f has run (a timer has no future)
Future.delayed(d, f) from the event queue, after d when f has run
Future.pause(d) (no function) after d, never with an error

Future.syncValue (3.10) and Future.pause (3.13)

Future.syncValue(v) arrived in Dart 3.10: unlike Future.value, it never accepts another future as its value. Future.pause(d), a static method added in Dart 3.13, waits for d without running any code and cannot complete with an error, which makes await Future.pause(Duration.zero) a plain way to let the loop run other work.

Checkpoint

Check: Futures and their chains

  1. Question 1 of 2 In price_chain.dart, fetchPrice('ink') throws inside its delayed callback. Which steps of the chain run?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: catchError and whenComplete, and neither then

    The output shows ink: lookup failed, then ink: done, and no discounted price for ink.

  2. Question 2 of 2 Which statement of synchronous code behaves most like whenComplete on a future?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: finally

    price_chain.dart printed done for pen, which succeeded, and for ink, which failed.

An error nobody handles still ends the program

A future’s error does not disappear when nobody listens for it. Here main starts a delayed computation that fails, and nothing calls catchError on it:

unhandled_future.dart: a failed future that nobody handles Dart · unhandled_future.dart
// A future fails after main has returned, and nothing handles its error.
void main() {
  Future.delayed(const Duration(milliseconds: 10), () => throw StateError('stock service down'));
  print('main has run its last line');
}

Output (exit status 255)

main has run its last line

Printed as an error (standard error)

Unhandled exception:
Bad state: stock service down
#0      main.<anonymous closure> (unhandled_future.dart:3:58)
#1      new Future.delayed.<anonymous closure> (dart:async/future.dart:448:42)
#2      Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
#3      _Timer._runTimers (dart:isolate-patch/timer_impl.dart:423:19)
#4      _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:454:5)
#5      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run unhandled_future.dart

main printed its last line and returned. Ten milliseconds later the timer ran, the function threw, and because nothing handled the future’s error, the runtime reported Unhandled exception: Bad state: stock service down and ended the program with exit code 255, the same way as for an exception thrown in synchronous code. The trace starts in the closure on line 3, then shows the timer frames of the event loop that called it, not main, because main had already returned.

Starting a future and dropping it

fetchPrice('ink'); on a line of its own starts the lookup, but its error has nowhere to go. Keep the future and handle it: chain .catchError(...) (or use try with await, the subject of the next lesson), so a failure is reported where you decide.

Checkpoint

Check: Errors nobody handles

  1. One question unhandled_future.dart prints its last line and returns from main; then the delayed future fails and nothing handles it. With which exit code does the program end?

    Type a number.

    Show the answer: One question

    Answer: 255

    An error that no handler takes is reported as an unhandled exception, and the dart command ends the program with 255, even though main had already returned.

A Completer turns a callback API into a future

Many older libraries report back through callbacks: read(onReading, onFailure). A Completer: A dart:async object that creates a future and completes it later with complete(value) or completeError(error), often from the callbacks of an API that does not return futures. It completes only once. adapts them. You create a Completer<T>, hand its future to your caller, and call complete(value) or completeError(error) from the callbacks:

completer.dart: a callback scale API as a Future Dart · completer.dart
// A Completer turns a callback-style API into a Future.
import 'dart:async';

class ScaleException implements Exception {
  ScaleException(this.reason);
  final String reason;
  @override
  String toString() => 'ScaleException: $reason';
}

/// A callback-style API, the shape of many older libraries: it answers through onReading or onFailure.
class Scale {
  var _reads = 0;

  void read(void Function(double grams) onReading, void Function(String reason) onFailure) {
    Timer(const Duration(milliseconds: 15), () {
      if (_reads++ == 0) {
        onReading(412.5);
      } else {
        onFailure('not settled');
      }
    });
  }
}

Future<double> readGrams(Scale scale) {
  final completer = Completer<double>();
  scale.read(
    completer.complete,
    (reason) => completer.completeError(ScaleException(reason)),
  );
  return completer.future;
}

void main() {
  final scale = Scale();
  readGrams(scale)
      .then((grams) => print('first reading: $grams g'))
      .then((_) => readGrams(scale))
      .then((grams) => print('second reading: $grams g'))
      .catchError((Object e) => print('second reading failed: $e'));

  final once = Completer<int>();
  once.complete(1);
  try {
    once.complete(2);
  } on StateError catch (e) {
    print('completing twice: $e');
  }
}

Output

completing twice: Bad state: Future already completed
first reading: 412.5 g
second reading failed: ScaleException: not settled

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run completer.dart

The first reading arrived as 412.5 g, and the second callback reported a failure, which completeError turned into a ScaleException for catchError. The first line of output, though, is the last block of main: it ran synchronously, before any timer, and shows the one rule a Completer enforces. A future completes once, so a second complete throws Bad state: Future already completed, a StateError. A callback API that may call back twice must check isCompleted first.

Reach for a Completer only at that edge. The Completer documentation points out that most futures are simpler to make with a Future constructor or a chain of then calls, and keeps the Completer for building a future from scratch, as when a callback-based API is turned into a future-based one.

Checkpoint

Check: Completers for callback APIs

  1. One question A callback API sometimes calls onDone twice, and your wrapper calls completer.complete in onDone. What happens the second time?

    Choose one answer.

    Show the answer: One question

    Answer: complete throws a StateError, Future already completed

    Check completer.isCompleted before completing when the callback can fire more than once.

Long synchronous work holds up every callback

Because one thread runs one callback at a time, a long stretch of synchronous code holds up everything else in its isolate: timers, I/O callbacks, microtasks and the rest of the program. This program makes the delay certain by working for 300 ms in a loop that checks a Stopwatch:

blocking.dart: 300 ms of work delays a 10 ms timer Dart · blocking.dart
// 300 ms of synchronous work holds up a timer that was due after 10 ms.
import 'dart:async';

void main() {
  final clock = Stopwatch()..start();
  Timer(const Duration(milliseconds: 10), () {
    final late = clock.elapsedMilliseconds >= 300;
    print('the 10 ms timer ran ${late ? 'only after the 300 ms of work' : 'on time'}');
  });
  while (clock.elapsedMilliseconds < 300) {
    // Busy work: nothing else on this isolate can run until the loop ends.
  }
  print('busy work done after 300 ms');
}

Output

busy work done after 300 ms
the 10 ms timer ran only after the 300 ms of work

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run blocking.dart

The timer was due after 10 ms and ran only after the busy loop had ended. In a server, every request waiting for that isolate waits as long; in an app, the screen cannot be redrawn. The concurrency page’s remedy for work that takes long is to run it in another isolate, which has its own thread and event loop, so the main isolate can keep answering.

Checkpoint

Check: Work that blocks the loop

  1. One question A server isolate parses a large upload in a synchronous loop that takes 3 seconds. What happens to a request that arrives at the same isolate one second in?

    Choose one answer.

    Show the answer: One question

    Answer: It waits until the loop ends, about 2 more seconds, before any of its code runs

    blocking.dart showed a timer due after 10 ms running only after 300 ms of work. Long work belongs in another isolate.

Practise: predict, then run

Each change below has a definite answer. Write your prediction down, then run the changed program with dart run <file>.dart:

  1. In async_order.dart, move the scheduleMicrotask line below the Future.value line. Which two numbers swap?
  2. Add Future.microtask(() => print('M')); as the first line of main. Where does M appear?
  3. In price_chain.dart, delete the catchError line. What does the program print now, and with which exit code?
  4. In blocking.dart, try delays of 0, 1 and 10,000 ms for the timer. Can any of them print the timer’s line before busy work done? Explain the answer with the event loop.

Test your model of the loop here: each answer is explained as soon as you choose it, and your weakest topic comes up first:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 How many pieces of your code can run at the same moment inside one Dart isolate?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: One

    The event loop hands the isolate's one thread to one piece of code at a time.

  2. Question 2 of 12 Two timer callbacks both update the same list. Can one callback see the list half-updated by the other?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: No, because each callback runs to its end before the other starts

    A callback is never interrupted halfway, so no other code in the isolate sees its intermediate state.

  3. Question 3 of 12 When does the event loop run the microtasks that are waiting?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: Whenever the running code ends, before it takes the next event

    This is why async_order.dart printed 4 and 5 before 6 and 7.

  4. Question 4 of 12 main calls Future.delayed(Duration.zero, () => print('D')), then Future.microtask(() => print('M')), then Future.sync(() => print('S')). What is printed?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: S, M, D

    Sync first, then microtasks, then events: the rule of the two queues.

  5. Question 5 of 12 What does then return?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: A new future for the result of its callback

    price_chain.dart chains two then steps; the second receives 1080, the first step's result.

  6. Question 6 of 12 You want a function to wait 50 ms and do nothing else. Which call fits best on Dart 3.13?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Future.pause(Duration(milliseconds: 50))

    Future.pause arrived in Dart 3.13 as a Future.delayed without a callback.

  7. Question 7 of 12 Why did unhandled_future.dart print main's last line before the error?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: The failing function ran later, as a timer event after main had returned

    The trace starts in the closure on line 3 and shows timer frames, not main, because main had already ended.

  8. Question 8 of 12 A CLI starts a background save with saveDraft(); and sometimes ends with exit code 255 after printing its result. What is the smallest fix that keeps the save in the background?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: Attach an error handler, saveDraft().catchError(...), that reports the failure

    A synchronous try cannot catch a future's later error; the future itself needs a handler.

  9. Question 9 of 12 Which pair of Completer calls finishes its future with a value or with an error?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: complete(value) and completeError(error)

    completer.dart called complete from onReading and completeError from onFailure.

  10. Question 10 of 12 You wrap scale.read(onReading, onFailure) in Future<double> readGrams(). What must readGrams return?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: completer.future, returned at once, before any callback has run

    Hand out the future and keep the Completer private, so only the callbacks can complete it.

  11. Question 11 of 12 What does a long synchronous loop in an isolate hold up?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: Every timer, I/O callback and microtask of that isolate

    blocking.dart's timer, due after 10 ms, ran only after 300 ms of work.

  12. Question 12 of 12 Which remedy does the Dart concurrency page give for work that takes long enough to hold up the main isolate?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: Run the work in another isolate

    Moving work into a future changes when it starts, not where it runs; only another isolate adds a thread.

Interview questions

Warm-up (fresher): What sets Dart’s microtask queue apart from its event queue?

Both hold callbacks that the event loop runs later, one at a time, on the isolate’s single thread. The event queue holds outside work: timer callbacks, finished I/O and messages from other isolates. The microtask queue holds short follow-up jobs, such as a then callback on a future that has already completed or a scheduleMicrotask call. The difference is priority. Each time the running code ends, the loop runs every waiting microtask, in order, before it takes the next event, so a microtask scheduled now runs before a timer that was due earlier. In async_order.dart both microtasks, 4 and 5, printed before either event, 6 and 7.

Warm-up (fresher): What problem does a Completer solve?

It gives you a future for a result that arrives through a callback, so code written with futures can use an API that was not. You create a Completer<T>, return its future at once, and call complete(value) from the success callback or completeError(error) from the failure callback. Callers then chain then and catchError, or use await, as with any other future. A completer completes only once: a second complete throws a StateError, so a callback that can fire twice must check isCompleted.

The bank holds two longer questions where the loop’s rules decide a real failure:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A cache refresher in a Dart server walks 50,000 entries and, after each one, schedules the next entry with scheduleMicrotask so that it "does not block". After the deploy, request timeouts fire seconds late and socket reads stall during every refresh. Explain what is happening and fix the refresher.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A command line tool parses a 60 MB CSV file in one synchronous loop that takes about 4 seconds, while a Timer.periodic is meant to redraw a progress line every 200 ms. The line does not move until the very end. Explain the freeze and compare two ways to keep the progress line alive.

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Why did a timer due in 10 ms run only after a 300 ms loop had ended?

    Each isolate runs one piece of code at a time on one thread, and its event loop hands out the turns: a callback waits until the running code ends.

  2. When do microtasks run compared with timers and I/O events?

    After main and after every event, the loop empties the microtask queue in order before it takes the next event, so a microtask always runs before a timer that is already waiting.

  3. What goes in the event queue and what in the microtask queue?

    The event queue holds outside work: timer callbacks, finished I/O and messages from other isolates. The microtask queue holds short follow-up jobs, such as scheduleMicrotask calls, and it has priority.

  4. How do then, catchError and whenComplete treat a future that fails?

    A future completes once, with a value or an error. then chains steps, an error skips them until a catchError, and whenComplete runs on both paths.

  5. What happens to the error of a future that nobody handles?

    It is reported as an unhandled exception and ends a command-line program with exit code 255, even after main has returned. Keep the future and chain catchError, or await it in a try.

  6. What does a Completer do?

    It turns a callback API into a future: return its future at once, then call complete(value) or completeError(error) from the callbacks. It completes only once; a second complete throws a StateError.

  7. What do Future.syncValue and Future.pause do?

    Future.syncValue(v), from Dart 3.10, never accepts another future as its value, unlike Future.value. Future.pause(d), from Dart 3.13, waits for d without running code and never completes with an error.

Key takeaways

  • Each isolate runs one piece of code at a time on one thread, and its event loop hands out the turns: main first, then one waiting callback after another, each run to its end.
  • After main and after every event, the loop runs all waiting microtasks in order, so microtasks always come before the next timer, I/O or message event.
  • A future completes once, with a value or an error; then chains steps, an error skips them until catchError, and whenComplete runs on both paths.
  • A future’s error that nobody handles is reported as an unhandled exception and ends a command line program with exit code 255, even after main has returned.
  • A Completer turns a callback API into a future, with complete and completeError, and it completes only once.
  • Long synchronous work holds up every timer and callback in its isolate; move it to another isolate.

Terms in this lesson

Completer
A dart:async object that creates a future and completes it later with complete(value) or completeError(error), often from the callbacks of an API that does not return futures. It completes only once.
event loop
The loop inside each Dart isolate that runs one piece of code at a time: main first, then each waiting microtask, then the next event, each to its end, until nothing is left to wait for.
event queue
The queue of outside work an isolate's event loop takes one item at a time: timer callbacks, finished I/O and messages from other isolates, in the order they arrived.
future
An object of type Future<T> that completes once, later, with a value of type T or with an error. Callbacks registered with then, catchError and whenComplete, or an await, receive the result.
microtask
A short follow-up job, such as a then callback of a completed future or a scheduleMicrotask call. The event loop runs every waiting microtask, in order, before it takes the next event.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.