Async Basics and Futures
By Syncfusion®, Inc. · Last updated
How Dart runs slow work without freezing: the event loop, Futures and their states, then and catchError, async and await, and waiting for several Futures with wait, any and timeout.
What Is Asynchronous Programming?
- Synchronous (sync) code runs one line after another. Each line waits for the one before to finish.
- Asynchronous (async) code can start a slow job, move on, and come back when the job is done.
- Blocking = the program stops and waits, doing nothing else.
- Non-blocking = the program keeps working while the slow job runs.
Simple lesson: async code starts slow work, keeps going, and picks up the result later.
A Real-World Example
Think of ordering food at the school canteen.
- Sync way: you stand at the counter until your dosa is ready. The whole line behind you waits.
- Async way: you get a token, sit down, and chat with friends. When your number is called, you collect the dosa.
- The token is a promise of food later. In Dart, that token is called a Future.
Every Run box on this page runs on the course's Dart runner in your browser.
The runner fast-forwards waiting time. A 3-second delay finishes at once, but always in the right order.
What You Will Learn
In this lesson, you will learn:
- How Dart's event loop decides what runs next.
- What a Future is, and its states.
- How to create Futures, and handle them with
then,catchErrorandwhenComplete. - How
asyncandawaitmake async code read like normal code. - How to wait for many Futures, and how to give up with
timeout.
The Event Loop
One Thing at a Time
Dart runs your code on one thread. It never runs two lines at the same moment. To handle slow jobs, it uses an event loop.
- Event loop = a loop that keeps taking the next job and running it.
- Event queue = jobs like timers, finished downloads, and
Future(...)work. - Microtask queue = small, urgent jobs. They always run before the next event.
The Order of Things
Step through this program and watch the queues:
Sync first, then microtasks, then events
import 'dart:async';
void main() {
print('1. main starts');
Future(() => print('5. event queue'));
scheduleMicrotask(() => print('3. microtask'));
Future.microtask(() => print('4. another microtask'));
print('2. main ends');
}
mainruns to the end first: 1, 2.- Then all microtasks: 3, 4.
- Then the event queue: 5.
A long loop in your code blocks the event loop. While it runs, no Future can finish and the app cannot react to taps.
Keep each job short. For heavy work, use an isolate (see Isolates).
Simple lesson: Dart finishes the current code, then empties the microtask queue, then takes the next event.
Futures
What Is a Future?
Future = an object that will hold a value later. Future<int> will give an int later.
A Future is always in one of these states:
| State | Means | Canteen token |
|---|---|---|
| Uncompleted | the work is still going | dosa is cooking |
| Completed with a value | the work finished well | "Number 42, your dosa!" |
| Completed with an error | the work failed | "Sorry, no dosa batter" |
Creating Futures
| Code | Gives | Use |
|---|---|---|
Future(() => work()) | runs work later, from the event queue | start work later |
Future.value(5) | a Future already holding 5 | a ready answer |
Future.delayed(Duration(seconds: 2), () => 5) | 5 after 2 seconds | waiting, timers, demos |
Future.error(StateError('no batter')) | a Future that fails | testing error paths |
A Future that finishes after a delay
Future<String> cookDosa() {
return Future.delayed(Duration(seconds: 3), () => 'Masala dosa');
}
void main() {
print('Order placed');
cookDosa().then((food) => print('Got: $food'));
print('Chatting with friends...');
}
Chatting with friends... prints before Got: Masala dosa. main did not wait.
Simple lesson: a Future is a token for a value that is not ready yet.
then, catchError and whenComplete
| Method | Runs when | Like |
|---|---|---|
.then((value) { ... }) | the Future gives a value | try |
.catchError((e) { ... }) | the Future fails | catch |
.whenComplete(() { ... }) | either way | finally |
Handling success, failure and both
Future<int> fetchMarks(String name) {
return Future.delayed(Duration(seconds: 1), () {
if (name == 'Unknown') {
throw StateError('No such student');
}
return 88;
});
}
void main() {
fetchMarks('Asha')
.then((m) => print('Asha: $m'))
.whenComplete(() => print('First request done'));
fetchMarks('Unknown')
.then((m) => print('Marks: $m'))
.catchError((e) => print('Failed: ${e.message}'))
.whenComplete(() => print('Second request done'));
}
Chaining
Each then returns a new Future, so you can chain steps:
A chain of then steps
void main() {
Future.value(40)
.then((marks) => marks + 5)
.then((marks) => marks * 2)
.then((total) => print('Final score: $total'));
}
Simple lesson: then handles the value, catchError handles failure, and whenComplete runs either way.
async and await
The Easy Way
Long then chains are hard to read. async and await let you write the same thing like normal code:
asyncafter the parameter list marks a function as async. It always returns a Future.awaitwaits for a Future inside an async function, and gives you its value.- While it waits, the event loop keeps running other jobs. Nothing freezes.
await pauses this function only
Refactoring then into await
The same steps, two styles
Future<int> getMarks() => Future.delayed(Duration(seconds: 1), () => 72);
Future<int> getBonus() => Future.delayed(Duration(seconds: 1), () => 8);
void withThen() {
getMarks().then((m) {
getBonus().then((b) {
print('then style: ${m + b}');
});
});
}
Future<void> withAwait() async {
var m = await getMarks();
var b = await getBonus();
print('await style: ${m + b}');
}
Future<void> main() async {
withThen();
await withAwait();
}
try and catch in Async Code
With await, a failed Future throws at that line. So a normal try works (see Exception Handling):
Catching a failed Future with try
Future<int> fetchFee(String name) async {
await Future.delayed(Duration(seconds: 1));
if (name.isEmpty) {
throw ArgumentError('Name is empty');
}
return 1200;
}
Future<void> main() async {
try {
var fee = await fetchFee('');
print('Fee: $fee');
} on ArgumentError catch (e) {
print('Could not fetch: ${e.message}');
} finally {
print('Request finished');
}
}
await only works inside a function marked async.
Forgetting await is a common bug: you get the Future itself, not its value. The next line runs too early.
Simple lesson: mark the function async, put await before each Future, and use try for errors.
Many Futures at Once
Future.wait
Future.wait([...]) waits for all Futures, and gives a list of their values, in order.
Three requests, waited for together
Future<int> marks(String subject, int value, int seconds) =>
Future.delayed(Duration(seconds: seconds), () => value);
Future<void> main() async {
var all = await Future.wait([
marks('Maths', 92, 3),
marks('Science', 85, 1),
marks('English', 78, 2),
]);
print(all);
print('Total: ${all.reduce((a, b) => a + b)}');
}
- The three run at the same time, so the total wait is 3 seconds, not 6.
- The list keeps the order you asked, not the order they finished.
Future.any and timeout
Future.any([...])gives the value of the first Future to finish..timeout(duration)fails with aTimeoutExceptionif the Future takes too long.
The fastest server, and giving up
import 'dart:async';
Future<String> server(String name, int seconds) =>
Future.delayed(Duration(seconds: seconds), () => name);
Future<void> main() async {
var fastest = await Future.any([server('Mumbai', 3), server('Chennai', 1)]);
print('Fastest: $fastest');
try {
var slow = await server('Delhi', 10).timeout(Duration(seconds: 2));
print(slow);
} on TimeoutException {
print('Delhi took too long. Showing saved data.');
}
}
TimeoutException comes from dart:async, so that import is needed.
Easy way to remember
- Future = a token for "later".
- then / catchError / whenComplete = try / catch / finally for tokens.
- async + await = wait for the token, but let others keep working.
- wait = all of them. any = the first. timeout = not forever.
Simple lesson: wait for all, any for the fastest, and timeout so you never wait forever.
TL;DR
- Async = start slow work, keep going, use the result later. Dart uses one thread and an event loop.
- Order: current code, then microtasks, then events.
Future = a value later: uncompleted, completed with value, or with error.
- Make one with
Future(),.value,.delayed,.error.
- Make one with
then,catchError,whenComplete, or the easierasync/awaitwithtry.Future.wait(all),Future.any(first),.timeout(give up,TimeoutException).
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
Forgetting await | The call gives the value | It gives a Future | The next line uses a Future, not the value. |
await in a function without async | await works anywhere | Mark the function async | A compile error. |
| A long loop in an async app | Async means parallel | Dart runs one job at a time | The app freezes. |
| Awaiting one by one when they are independent | Order needs await | Use Future.wait | It takes much longer. |
No catchError or try | Futures cannot fail | A failed Future throws | Unhandled exception. |
Expecting Future(...) to run at once | It runs immediately | It runs later, from the event queue | Output order surprises you. |
Check Yourself
Exercises
What to Remember
- Async code starts slow work and keeps going. Dart runs one job at a time with an event loop.
- Current code runs first, then microtasks, then events.
- A Future is a value that comes later, or an error.
- Use
asyncandawaitwithtryfor clear async code. Future.waitwaits for all,Future.anytakes the first, andtimeoutstops waiting.
Next: Streams. You will handle many values that arrive one after another.