On this page22 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. Synchronous Code Runs Line by Line
- 2. Measuring Time With Stopwatch
- 3. Synchronous Code Blocks
- 4. Tasks That Wait
- 5. The Problem With Blocking
- 6. Asynchronous Code: Start Now, Finish Later
- 7. Output Order Is Not Code Order
- 8. The Event Loop
- 9. Async Does Not Make Work Faster
- 10. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 87 — Synchronous vs Asynchronous Programming
| Category | 15 — Asynchronous Programming |
| Lecture | 087 of 140 |
| Estimated video length | 10 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
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
Stopwatchare 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)
whileloops (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:
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.delayedandDuration - 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.
void main() {
print('Ali opens the app');
print('Ali checks his balance');
print('Ali logs out');
}Ali opens the app
Ali checks his balance
Ali logs outCalling 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.
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:
Total: 499999500000
Took 1 ms3. 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.
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:
Start
Next line runs at 2000 msprint('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:
| Task | What the program waits for |
|---|---|
| Loading a web page or calling an API | the network and a distant server |
| Reading or writing a file | the disk |
| A timer or a delay | the clock |
| Asking a database | another 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:
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:
Busy work starts
Busy work ends at 1000 ms
Timer fired at 1001 msThe 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):
void main() {
print('Order placed');
Future.delayed(Duration(seconds: 2), () {
print('Coffee is ready');
});
print('Reading a book while waiting');
}Order placed
Reading a book while waiting
Coffee is readyThe 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:
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');
}1. Start
2. End of main
3. Fast task
4. Slow taskNotice 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:
- Run
main()from top to bottom. - Tasks that are scheduled for later (timers, finished network calls, file reads) go into a queue.
- When the current code has finished, take the next ready task from the queue and run it.
- Repeat until the queue is empty, then exit.
That is why even a delay of zero comes last:
void main() {
Future.delayed(Duration.zero, () => print('C'));
print('A');
print('B');
}A
B
CDuration.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:
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:
Heavy task done
Quick task at 500 msThe 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.
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.');
}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
void main() {
print('Wake up');
print('Eat breakfast');
print('Go to school');
}Wake up
Eat breakfast
Go to schoolExample 2 — A Function Call Is Still Synchronous
void greet(String name) {
print('Hello, $name');
}
void main() {
print('Before');
greet('Zayan');
print('After');
}Before
Hello, Zayan
AfterExample 3 — Timing a Blocking Task
void main() {
final stopwatch = Stopwatch()..start();
while (stopwatch.elapsedMilliseconds < 300) {}
print('Blocked for ${stopwatch.elapsedMilliseconds} ms');
}One run printed:
Blocked for 300 msExample 4 — One Delayed Task
void main() {
Future.delayed(Duration(milliseconds: 500), () => print('Later'));
print('Now');
}Now
LaterExample 5 — Three Delays
void main() {
Future.delayed(Duration(milliseconds: 300), () => print('Third'));
Future.delayed(Duration(milliseconds: 100), () => print('First'));
Future.delayed(Duration(milliseconds: 200), () => print('Second'));
}First
Second
ThirdExample 6 — Zero Delay Still Waits Its Turn
void main() {
print('One');
Future.delayed(Duration.zero, () => print('Three'));
print('Two');
}One
Two
ThreeExample 7 — Student Results Arriving Later
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');
}Exam submitted by Ali
Ali goes home
Result: 240 / 300Example 8 — Shopping Total While Waiting for Delivery
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');
}Total: 870
Delivery bookedHow it works
- The delivery task is scheduled first but put in the queue for 800 ms.
- The loop runs synchronously and prints the total.
mainends; 800 ms later the queued task runs.
Example 9 — Bank Balance From a Slow Server
void main() {
print('Fetching balance for Sara...');
Future.delayed(Duration(seconds: 1), () {
final balance = 1000 - 250;
print('Balance: $balance');
});
print('Showing loading spinner');
}Fetching balance for Sara...
Showing loading spinner
Balance: 750Example 10 — Temperature Readings Every Half Second
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');
}Sensor started
Reading 1: 21.5 °C
Reading 2: 22.0 °C
Reading 3: 22.8 °CHow it works
- The loop schedules three tasks at 500, 1000, and 1500 ms, then finishes at once.
Sensor startedprints first, because it is synchronous.- 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
void main() {
var balance = 0;
Future.delayed(Duration(seconds: 1), () {
balance = 750;
});
print('Balance: $balance');
}Balance: 0The 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
// Future.delayed(2, () => print('Done')); // ✗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
// Future.delayed(Duration(seconds: 1), print('Done')); // ✗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
Stopwatchto 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:
Loading level...
Tip: collect the stars
Level ready!Try it yourself before opening the solution.
Show solution
void main() {
print('Loading level...');
Future.delayed(Duration(milliseconds: 1500), () {
print('Level ready!');
});
print('Tip: collect the stars');
}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
- Print
A,B,Csynchronously, each on its own line. - Use
Future.delayedto printHelloafter 1 second, and printWaiting...right away. - Write a
Durationof 1 minute and 30 seconds and print itsinSecondsvalue.
Application
-
Predict the output, then run it:
void main() { Future.delayed(Duration(milliseconds: 200), () => print('X')); print('Y'); Future.delayed(Duration(milliseconds: 100), () => print('Z')); } -
Use
Stopwatchand a busy loop to block for 200 ms, then printDone. -
Print
Ordering pizza for Sara, schedulePizza deliveredafter 2 seconds, and printSetting the tablein between. -
Store
[80, 75, 85]. After 500 ms, print the average asAverage: 80.0. -
Schedule
SecondwithDuration.zero, then printFirst. Which prints first, and why?
Challenge
- For the names
['Ali', 'Sara', 'Zayan'], scheduleWelcome, <name>at 300, 200, and 100 ms respectively, so they print in reverse list order. - Schedule
Reminderafter 100 ms, then busy-wait 400 ms with aStopwatch, and print the elapsed time inside the reminder. Explain why it is not 100.
Practice Answers
Question 1 — Solution
void main() {
print('A');
print('B');
print('C');
}A
B
CQuestion 2 — Solution
void main() {
Future.delayed(Duration(seconds: 1), () => print('Hello'));
print('Waiting...');
}Waiting...
HelloQuestion 3 — Solution
void main() {
final wait = Duration(minutes: 1, seconds: 30);
print(wait.inSeconds);
}90Question 4 — Solution
Y
Z
XY is synchronous. Z (100 ms) is due before X (200 ms).
Question 5 — Solution
void main() {
final stopwatch = Stopwatch()..start();
while (stopwatch.elapsedMilliseconds < 200) {}
print('Done');
}DoneQuestion 6 — Solution
void main() {
print('Ordering pizza for Sara');
Future.delayed(Duration(seconds: 2), () => print('Pizza delivered'));
print('Setting the table');
}Ordering pizza for Sara
Setting the table
Pizza deliveredQuestion 7 — Solution
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');
});
}Average: 80.0Question 8 — Solution
void main() {
Future.delayed(Duration.zero, () => print('Second'));
print('First');
}First
SecondThe delayed task goes into the queue; the event loop runs it only after main finishes.
Question 9 — Solution
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]}');
});
}
}Welcome, Zayan
Welcome, Sara
Welcome, AliQuestion 10 — Solution
void main() {
final clock = Stopwatch()..start();
Future.delayed(Duration(milliseconds: 100), () {
print('Reminder at ${clock.elapsedMilliseconds} ms');
});
while (clock.elapsedMilliseconds < 400) {}
}One run printed:
Reminder at 400 msThe 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
- What does "synchronous" mean?
- What is blocking code?
- Name three tasks that spend most of their time waiting.
- What two arguments does
Future.delayedtake? - Why does
Duration.zerostill run after the rest ofmain? - How many threads run your Dart code by default?
- 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 aDuration.- 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.