Watch the video (10 min)
On this page22 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. Synchronous Code Runs Line by Line
  5. 2. Measuring Time With Stopwatch
  6. 3. Synchronous Code Blocks
  7. 4. Tasks That Wait
  8. 5. The Problem With Blocking
  9. 6. Asynchronous Code: Start Now, Finish Later
  10. 7. Output Order Is Not Code Order
  11. 8. The Event Loop
  12. 9. Async Does Not Make Work Faster
  13. 10. A Real-World Example
  14. Code Examples
  15. Common Beginner Mistakes
  16. Best Practices
  17. Mini Challenge
  18. Practice Questions
  19. Practice Answers
  20. Quick Revision
  21. Key Takeaways
  22. What's Next?

Lecture 87 — Synchronous vs Asynchronous Programming

Category15 — Asynchronous Programming
Lecture087 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelIntermediate

Every program so far ran one line after another, and a slow line made everything behind it wait. Asynchronous code lets a program start a slow task and keep working until the result is ready.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2. Timings measured with Stopwatch are labelled one run printed — your numbers will differ by a few milliseconds.


Prerequisites

Students should already understand, from Lectures 001–086:

  • functions, including anonymous and arrow functions (Lectures 030–034)
  • passing a function to another function (Lectures 035–036)
  • while loops (Lecture 028)
  • classes, objects, and the cascade operator .. (Lectures 020, 053)
  • exceptions and how a program stops on an error (Lectures 083–086)

Connection With Previous Lectures

Every program you have written works like this:

dart
void main() {
  print('Step 1');
  print('Step 2');
}

Line 2 starts only when line 1 has finished. That is fine when every line is instant. But real programs wait: for a server to answer, for a file to load, for a timer to run out. If waiting means standing still, the whole program freezes for the full wait. This lecture shows that problem and Dart's answer to it.


Learning Objectives

By the end of this lecture you will be able to:

  • Explain what synchronous code is and why it runs line by line
  • Recognize tasks that spend most of their time waiting
  • Measure how long code takes with Stopwatch
  • Explain why blocking code freezes a program
  • Explain what asynchronous code is and how it lets a program continue
  • Schedule work for later with Future.delayed and Duration
  • Predict the order of output when synchronous and delayed code are mixed
  • Describe the event loop: one thread, one queue of waiting tasks

1. Synchronous Code Runs Line by Line

Synchronous code runs one statement at a time, in order. Each statement must finish before the next one starts.

dart
void main() {
  print('Ali opens the app');
  print('Ali checks his balance');
  print('Ali logs out');
}
output
Ali opens the app
Ali checks his balance
Ali logs out

Calling a function does not change this: Dart runs the whole function body, then returns and carries on with the next line.


2. Measuring Time With Stopwatch

To see waiting, we need a clock. Stopwatch (from dart:core, no import needed) measures elapsed time. start() begins counting; elapsedMilliseconds reads the time so far.

dart
void main() {
  final stopwatch = Stopwatch()..start();
  var total = 0;
  for (var i = 0; i < 1000000; i++) {
    total += i;
  }
  print('Total: $total');
  print('Took ${stopwatch.elapsedMilliseconds} ms');
}

One run printed:

output
Total: 499999500000
Took 1 ms

3. Synchronous Code Blocks

A busy-wait loop does nothing except keep the computer occupied until a time has passed. We use it to imitate a slow task.

dart
void slowTask() {
  final stopwatch = Stopwatch()..start();
  while (stopwatch.elapsedMilliseconds < 2000) {}
}

void main() {
  final clock = Stopwatch()..start();
  print('Start');
  slowTask();
  print('Next line runs at ${clock.elapsedMilliseconds} ms');
}

One run printed:

output
Start
Next line runs at 2000 ms

print('Next line …') waited the full two seconds. Code that keeps everything behind it waiting is called blocking code.


4. Tasks That Wait

Many slow tasks are not working hard — they are waiting:

TaskWhat the program waits for
Loading a web page or calling an APIthe network and a distant server
Reading or writing a filethe disk
A timer or a delaythe clock
Asking a databaseanother program

Analogy — ordering coffee. A synchronous café makes you stand at the counter, staring at the machine, until your cup is ready; nobody else can order. An asynchronous café gives you a ticket, you sit down and read, and they call your name when the coffee is done.


5. The Problem With Blocking

While a program blocks, it can do nothing else — not even work that is already due. Here a task is scheduled to run after 100 ms, then a one-second busy loop starts:

dart
void main() {
  final timer = Stopwatch()..start();
  Future.delayed(Duration(milliseconds: 100), () {
    print('Timer fired at ${timer.elapsedMilliseconds} ms');
  });
  print('Busy work starts');
  while (timer.elapsedMilliseconds < 1000) {}
  print('Busy work ends at ${timer.elapsedMilliseconds} ms');
}

One run printed:

output
Busy work starts
Busy work ends at 1000 ms
Timer fired at 1001 ms

The timer was due at 100 ms but fired at about 1000 ms — it had to wait until the busy loop let go. In an app, that is a frozen screen: buttons stop responding until the loop ends.


6. Asynchronous Code: Start Now, Finish Later

Asynchronous code starts a task and does not wait for it. The program moves on, and the task's result is handled later, when it is ready.

Future.delayed is a simple way to see this. It takes a Duration (how long to wait) and a function (what to run afterwards):

dart
void main() {
  print('Order placed');
  Future.delayed(Duration(seconds: 2), () {
    print('Coffee is ready');
  });
  print('Reading a book while waiting');
}
output
Order placed
Reading a book while waiting
Coffee is ready

The last print ran immediately; it did not wait two seconds. Future.delayed returns a Future — we will study Futures properly in Lecture 088.

Duration describes a length of time with named parameters: Duration(seconds: 2), Duration(milliseconds: 500), Duration(minutes: 1, seconds: 30).


7. Output Order Is Not Code Order

With asynchronous code, the order lines are written is not always the order they run. Shorter delays run first:

dart
void main() {
  print('1. Start');
  Future.delayed(Duration(milliseconds: 300), () => print('4. Slow task'));
  Future.delayed(Duration(milliseconds: 100), () => print('3. Fast task'));
  print('2. End of main');
}
output
1. Start
2. End of main
3. Fast task
4. Slow task

Notice that main() finished before either task ran. The program does not exit when main ends — Dart keeps it alive until every pending task has run.


8. The Event Loop

How can one program "wait" and "keep working" at once? Dart runs your code on a single thread — one line at a time — plus an event loop:

  1. Run main() from top to bottom.
  2. Tasks that are scheduled for later (timers, finished network calls, file reads) go into a queue.
  3. When the current code has finished, take the next ready task from the queue and run it.
  4. Repeat until the queue is empty, then exit.

That is why even a delay of zero comes last:

dart
void main() {
  Future.delayed(Duration.zero, () => print('C'));
  print('A');
  print('B');
}
output
A
B
C

Duration.zero means "as soon as possible" — but "possible" only starts after the current code finishes. It also explains section 5: the loop picks a new task only when the running code ends, so a busy loop holds up the whole queue.

Dart actually has two queues: a small, high-priority microtask queue and the normal event queue. We will study both in Lecture 097 — Dart Concurrency Model.


9. Async Does Not Make Work Faster

Asynchronous code helps with waiting, not with heavy calculating. A busy loop inside a delayed task still blocks everything queued behind it:

dart
void main() {
  final clock = Stopwatch()..start();
  Future.delayed(Duration.zero, () {
    while (clock.elapsedMilliseconds < 500) {}
    print('Heavy task done');
  });
  Future.delayed(Duration(milliseconds: 10), () {
    print('Quick task at ${clock.elapsedMilliseconds} ms');
  });
}

One run printed:

output
Heavy task done
Quick task at 500 ms

The quick task was due at 10 ms but ran at 500 ms. Heavy calculations need a second thread — we will study isolates in Lecture 098.


10. A Real-World Example

Sara orders a notebook online. Payment takes time to confirm, but the app keeps showing her cart in the meantime.

dart
void main() {
  final cart = ['Notebook', 'Pen', 'Ruler'];
  print('Sending payment for ${cart.length} items...');

  Future.delayed(Duration(seconds: 1), () {
    print('Payment confirmed. Thank you, Sara!');
  });

  for (final item in cart) {
    print('In your cart: $item');
  }
  print('You can keep browsing.');
}
output
Sending payment for 3 items...
In your cart: Notebook
In your cart: Pen
In your cart: Ruler
You can keep browsing.
Payment confirmed. Thank you, Sara!

The slow step was started first, the synchronous work ran straight away, and the result arrived when it was ready.


Code Examples

Example 1 — Synchronous Order

dart
void main() {
  print('Wake up');
  print('Eat breakfast');
  print('Go to school');
}
output
Wake up
Eat breakfast
Go to school

Example 2 — A Function Call Is Still Synchronous

dart
void greet(String name) {
  print('Hello, $name');
}

void main() {
  print('Before');
  greet('Zayan');
  print('After');
}
output
Before
Hello, Zayan
After

Example 3 — Timing a Blocking Task

dart
void main() {
  final stopwatch = Stopwatch()..start();
  while (stopwatch.elapsedMilliseconds < 300) {}
  print('Blocked for ${stopwatch.elapsedMilliseconds} ms');
}

One run printed:

output
Blocked for 300 ms

Example 4 — One Delayed Task

dart
void main() {
  Future.delayed(Duration(milliseconds: 500), () => print('Later'));
  print('Now');
}
output
Now
Later

Example 5 — Three Delays

dart
void main() {
  Future.delayed(Duration(milliseconds: 300), () => print('Third'));
  Future.delayed(Duration(milliseconds: 100), () => print('First'));
  Future.delayed(Duration(milliseconds: 200), () => print('Second'));
}
output
First
Second
Third

Example 6 — Zero Delay Still Waits Its Turn

dart
void main() {
  print('One');
  Future.delayed(Duration.zero, () => print('Three'));
  print('Two');
}
output
One
Two
Three

Example 7 — Student Results Arriving Later

dart
void main() {
  print('Exam submitted by Ali');
  Future.delayed(Duration(seconds: 1), () {
    final marks = [80, 75, 85];
    final total = marks.reduce((a, b) => a + b);
    print('Result: $total / 300');
  });
  print('Ali goes home');
}
output
Exam submitted by Ali
Ali goes home
Result: 240 / 300

Example 8 — Shopping Total While Waiting for Delivery

dart
void main() {
  Future.delayed(Duration(milliseconds: 800), () {
    print('Delivery booked');
  });
  final prices = [500, 250, 120];
  var total = 0;
  for (final price in prices) {
    total += price;
  }
  print('Total: $total');
}
output
Total: 870
Delivery booked

How it works

  1. The delivery task is scheduled first but put in the queue for 800 ms.
  2. The loop runs synchronously and prints the total.
  3. main ends; 800 ms later the queued task runs.

Example 9 — Bank Balance From a Slow Server

dart
void main() {
  print('Fetching balance for Sara...');
  Future.delayed(Duration(seconds: 1), () {
    final balance = 1000 - 250;
    print('Balance: $balance');
  });
  print('Showing loading spinner');
}
output
Fetching balance for Sara...
Showing loading spinner
Balance: 750

Example 10 — Temperature Readings Every Half Second

dart
void main() {
  final readings = [21.5, 22.0, 22.8];
  for (var i = 0; i < readings.length; i++) {
    Future.delayed(Duration(milliseconds: 500 * (i + 1)), () {
      print('Reading ${i + 1}: ${readings[i]} °C');
    });
  }
  print('Sensor started');
}
output
Sensor started
Reading 1: 21.5 °C
Reading 2: 22.0 °C
Reading 3: 22.8 °C

How it works

  1. The loop schedules three tasks at 500, 1000, and 1500 ms, then finishes at once.
  2. Sensor started prints first, because it is synchronous.
  3. Each task captures its own i (a closure, Lecture 037), so each prints its own reading.

Common Beginner Mistakes

1. Expecting delayed code to run in written order

Future.delayed schedules work; it does not pause. Code below it runs first.

2. Reading a value before the delayed task sets it

dart
void main() {
  var balance = 0;
  Future.delayed(Duration(seconds: 1), () {
    balance = 750;
  });
  print('Balance: $balance');
}
output
Balance: 0

The print ran before the task. Use the value inside the task for now — Lectures 088–089 show the proper way to wait for a result.

3. Passing a number instead of a Duration

dart
// Future.delayed(2, () => print('Done')); // ✗
output
Error: The argument type 'int' can't be assigned to the parameter type 'Duration'.

Write Duration(seconds: 2).

4. Calling the function instead of passing it

dart
// Future.delayed(Duration(seconds: 1), print('Done')); // ✗
output
Error: This expression has type 'void' and can't be used.

print('Done') runs immediately and gives back nothing. Pass a function: () => print('Done').

5. Using a busy loop to "wait"

A busy loop blocks the event loop and every queued task. Use Future.delayed for delays.


Best Practices

  • Treat anything that touches the network, files, or timers as asynchronous.
  • Never wait with a busy loop; schedule the work instead.
  • Keep each synchronous piece of work short so the event loop stays responsive.
  • Do not rely on written order once delays are involved — reason about when each task runs.
  • Use Stopwatch to measure, and remember timings vary from run to run.

Mini Challenge

Zayan's game shows Loading level..., then prints Tip: collect the stars straight away, and Level ready! after 1.5 seconds. Write it so the output is:

output
Loading level...
Tip: collect the stars
Level ready!

Try it yourself before opening the solution.

Show solution
dart
void main() {
  print('Loading level...');
  Future.delayed(Duration(milliseconds: 1500), () {
    print('Level ready!');
  });
  print('Tip: collect the stars');
}
output
Loading level...
Tip: collect the stars
Level ready!

The tip is synchronous, so it prints before the delayed task even though it is written after it.


Practice Questions

Work through these in order — they run from beginner to application to challenge.

Beginner

  1. Print A, B, C synchronously, each on its own line.
  2. Use Future.delayed to print Hello after 1 second, and print Waiting... right away.
  3. Write a Duration of 1 minute and 30 seconds and print its inSeconds value.

Application

  1. Predict the output, then run it:

    dart
    void main() {
      Future.delayed(Duration(milliseconds: 200), () => print('X'));
      print('Y');
      Future.delayed(Duration(milliseconds: 100), () => print('Z'));
    }
  2. Use Stopwatch and a busy loop to block for 200 ms, then print Done.

  3. Print Ordering pizza for Sara, schedule Pizza delivered after 2 seconds, and print Setting the table in between.

  4. Store [80, 75, 85]. After 500 ms, print the average as Average: 80.0.

  5. Schedule Second with Duration.zero, then print First. Which prints first, and why?

Challenge

  1. For the names ['Ali', 'Sara', 'Zayan'], schedule Welcome, <name> at 300, 200, and 100 ms respectively, so they print in reverse list order.
  2. Schedule Reminder after 100 ms, then busy-wait 400 ms with a Stopwatch, and print the elapsed time inside the reminder. Explain why it is not 100.

Practice Answers

Question 1 — Solution

dart
void main() {
  print('A');
  print('B');
  print('C');
}
output
A
B
C

Question 2 — Solution

dart
void main() {
  Future.delayed(Duration(seconds: 1), () => print('Hello'));
  print('Waiting...');
}
output
Waiting...
Hello

Question 3 — Solution

dart
void main() {
  final wait = Duration(minutes: 1, seconds: 30);
  print(wait.inSeconds);
}
output
90

Question 4 — Solution

output
Y
Z
X

Y is synchronous. Z (100 ms) is due before X (200 ms).

Question 5 — Solution

dart
void main() {
  final stopwatch = Stopwatch()..start();
  while (stopwatch.elapsedMilliseconds < 200) {}
  print('Done');
}
output
Done

Question 6 — Solution

dart
void main() {
  print('Ordering pizza for Sara');
  Future.delayed(Duration(seconds: 2), () => print('Pizza delivered'));
  print('Setting the table');
}
output
Ordering pizza for Sara
Setting the table
Pizza delivered

Question 7 — Solution

dart
void main() {
  final marks = [80, 75, 85];
  Future.delayed(Duration(milliseconds: 500), () {
    final average = marks.reduce((a, b) => a + b) / marks.length;
    print('Average: $average');
  });
}
output
Average: 80.0

Question 8 — Solution

dart
void main() {
  Future.delayed(Duration.zero, () => print('Second'));
  print('First');
}
output
First
Second

The delayed task goes into the queue; the event loop runs it only after main finishes.

Question 9 — Solution

dart
void main() {
  final names = ['Ali', 'Sara', 'Zayan'];
  for (var i = 0; i < names.length; i++) {
    final delay = 300 - i * 100;
    Future.delayed(Duration(milliseconds: delay), () {
      print('Welcome, ${names[i]}');
    });
  }
}
output
Welcome, Zayan
Welcome, Sara
Welcome, Ali

Question 10 — Solution

dart
void main() {
  final clock = Stopwatch()..start();
  Future.delayed(Duration(milliseconds: 100), () {
    print('Reminder at ${clock.elapsedMilliseconds} ms');
  });
  while (clock.elapsedMilliseconds < 400) {}
}

One run printed:

output
Reminder at 400 ms

The reminder was ready at 100 ms, but the busy loop blocked the single thread. The event loop could run the reminder only after the loop — and main — had finished.


Quick Revision

  1. What does "synchronous" mean?
  2. What is blocking code?
  3. Name three tasks that spend most of their time waiting.
  4. What two arguments does Future.delayed take?
  5. Why does Duration.zero still run after the rest of main?
  6. How many threads run your Dart code by default?
  7. Does asynchronous code make heavy calculations faster?

Key Takeaways

  • Synchronous code runs one statement at a time; a slow statement blocks everything after it.
  • Network calls, file access, and timers mostly wait — they should not freeze the program.
  • Asynchronous code starts a task, moves on, and handles the result later.
  • Future.delayed(duration, function) schedules a function to run after a Duration.
  • Output order can differ from code order; the program stays alive until queued tasks run.
  • Dart runs your code on one thread with an event loop that takes tasks from a queue only when the current code has finished.
  • Async helps with waiting, not with heavy calculation — a busy loop still blocks.

What's Next?

Lecture 088 — Futures. Future.delayed gave back something we ignored: a Future, a promise of a value that is not ready yet. Next, we will create Futures, attach code that runs when they complete, and finally get the value out instead of only printing inside the delayed function.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.