Dart (3.13) Module 11 – Asynchronous programming
async, await and asynchronous errors
Write Dart async functions with await and try, run calls in parallel with .wait and Future.wait, and handle the errors of futures nobody awaits.
What you will learn
- Write async functions that await futures and catch their errors with try and catch
- Run independent calls in parallel with a record's .wait and with Future.wait
- Handle a partial failure, a deadline and a retry without losing the results that arrived
- Prevent lost futures with lint rules, unawaited and a zone's error handler
Before you start
What you’ll learn · Before you start
What you will learn
- Write async functions that await futures and catch their errors with try and catch
- Run independent calls in parallel with a record's .wait and with Future.wait
- Handle a partial failure, a deadline and a retry without losing the results that arrived
- Prevent lost futures with lint rules, unawaited and a zone's error handler
Before you start
On this page
A checkout page needs a price and a stock count, and each comes from a call that takes 300 ms. Awaited one after the other they cost at least 600 ms; started together, about 300. Either call can also fail, and an error that escapes the wrong line ends the program.
This lesson writes async functions with await, catches asynchronous errors with an ordinary try, and waits for
several calls at once, with programs that ran on Dart 3.13.5.
An async function runs to its first await, then returns a future
Mark a function async and it may use await. Its body starts at once, inside the call, like any other function. At
its first await it stops and hands its caller a future, and it finishes later, when the awaited future completes:
// An async function runs synchronously up to its first await, hands its caller a future, and finishes later.
Future<int> fetchPrice(String sku) async {
print('fetchPrice: starts and runs to its first await');
await Future.pause(const Duration(milliseconds: 20));
print('fetchPrice: resumes after the await');
return 499;
}
Future<void> main() async {
print('main: calls fetchPrice');
final pending = fetchPrice('pen');
print('main: has ${pending.runtimeType}, is Future<int>: ${pending is Future<int>}');
final price = await pending;
print('main: price $price');
} Output
main: calls fetchPrice fetchPrice: starts and runs to its first await main: has Future<int>, is Future<int>: true fetchPrice: resumes after the await main: price 499
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run async_flow.dart
fetchPrice printed its first line before main got anything back. Then main held a Future<int> that had not
completed, and the rest of fetchPrice ran 20 ms later. An await never blocks the thread: it hands control back to
the event loop and resumes the function when the future completes. Step
through the messages:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- main calls fetchPrice. main prints its first line and calls fetchPrice. An async function starts at once, inside the call, like any other function.
- The body runs to its await. fetchPrice prints its first line synchronously. At the await it starts a 20 ms pause timer and stops there.
- main gets a future. Stopping at its first await makes fetchPrice return to main with a Future<int> that has not completed yet. main prints that it holds a future.
- main awaits the future. main reaches its own await. It pauses too, and the event loop can run other callbacks while both functions wait.
- The timer fires. After 20 ms the pause timer's event comes up. fetchPrice resumes on the line after its await and prints that it has resumed.
- The future completes. fetchPrice returns 499. That completes the future main is waiting for, so main resumes with 499 and prints the price.
An async function: A function marked async. It may use await, always returns a future, runs synchronously up to its first await and finishes later, when the futures it awaits have completed. always returns a future. return 499 in a function declared
Future<int> completes that future with 499, which is what the caller’s await produces.
Checkpoint
Check: Async functions and their first await
try and catch work across await
Asynchronous errors arrive after the call that caused them, but await brings them back to one line: when the awaited
future fails, the await throws its error right there. So the try, on, catch and finally of
exceptions and errors work unchanged:
// An awaited future's error is thrown at the await, so an ordinary try, on, catch and finally handle it.
import 'dart:async';
Future<int> fetchStock(String sku) async {
await Future.pause(const Duration(milliseconds: 10));
if (sku == 'ink') throw TimeoutException('stock service did not answer for "$sku"');
return 12;
}
Future<void> showStock(String sku) async {
print('$sku: asking');
try {
final count = await fetchStock(sku);
print('$sku: $count in stock');
} on TimeoutException catch (e) {
print('$sku: stock unknown (${e.message})');
} finally {
print('$sku: spinner stopped');
}
}
Future<void> main() async {
await showStock('pen');
await showStock('ink');
} Output
pen: asking pen: 12 in stock pen: spinner stopped ink: asking ink: stock unknown (stock service did not answer for "ink") ink: spinner stopped
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run await_errors.dart
For pen the stock arrived. For ink, fetchStock failed 10 ms after it was called, the await in showStock
threw the TimeoutException, and on TimeoutException handled it; finally stopped the spinner on both paths. That
is the main gain over then and catchError: asynchronous errors are handled with the same statements, in the same
order, as synchronous ones.
The same holds one level up. main awaited showStock twice, so if showStock itself had thrown, the error would
have reached main at that await, and with no try there the program would end with exit code 255.
Checkpoint
Check: try and catch around an await
Start the calls first, then await them together
Two awaits in a row wait one after the other: the second call does not start until the first has finished. When the
calls are independent, start both and wait for both. Put the futures in a record and call
.wait on it; the result is a record of values that a pattern unpacks:
// Two calls of 300 ms each: awaited one after the other, then started together and awaited as a record.
Future<int> fetchPrice(List<String> log) async {
log.add('price starts');
await Future.pause(const Duration(milliseconds: 300));
log.add('price ends');
return 499;
}
Future<int> fetchStock(List<String> log) async {
log.add('stock starts');
await Future.pause(const Duration(milliseconds: 300));
log.add('stock ends');
return 12;
}
Future<void> main() async {
final clock = Stopwatch()..start();
final one = <String>[];
final price1 = await fetchPrice(one);
final stock1 = await fetchStock(one);
final sequential = clock.elapsedMilliseconds;
print('one after the other: ${one.join(', ')}');
print(' price $price1, stock $stock1, at least 600 ms: ${sequential >= 600}');
clock.reset();
final both = <String>[];
final (price2, stock2) = await (fetchPrice(both), fetchStock(both)).wait;
final parallel = clock.elapsedMilliseconds;
print('started together: ${both.join(', ')}');
print(' price $price2, stock $stock2, under 600 ms: ${parallel < 600}');
} Output
one after the other: price starts, price ends, stock starts, stock ends price 499, stock 12, at least 600 ms: true started together: price starts, stock starts, price ends, stock ends price 499, stock 12, under 600 ms: true
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run waits.dart
The log shows the difference. In turn, the stock call started only after the price call had ended, so the pair took
at least 600 ms. Together, both calls were running before either ended, and the pair took under 600 ms, close to
the 300 ms of one call. Future.wait does the same for a list of futures of one type.
Three more tools finish the job. A ParallelWaitError reports which calls failed and keeps the values of the rest.
timeout gives up waiting after a deadline you choose, and a retry tries again after growing pauses. Lint rules, the
unawaited function and a zone: A dart:async context in which code and the callbacks it schedules run. A zone can bring its own handler for asynchronous errors that nothing else handles.’s error handler deal with the futures that nobody awaits.
Key idea
Start independent calls first and await them together with .wait; two awaits in a row run the calls one after the
other.
Checkpoint
Check: Waiting for calls together
Partial failures, timeouts and retries, with recorded runs Pro: opens with a Pro, Premium or Ultimate pass
One failed call should not throw away the answers that did arrive. This part handles a failure inside a parallel wait, puts a deadline on a slow call and retries a flaky one with growing pauses, each with a program that ran.
In this part:
- A ParallelWaitError keeps the values that arrived
- timeout stops the waiting, not the call
- Retry with growing pauses, and only for errors worth retrying
3 runnable examples (Dart) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Lost futures, the lint rules that find them and a zone for stray errors Pro: opens with a Pro, Premium or Ultimate pass
A future that nobody awaits takes its error out of reach of every try. This part reproduces the crash such a future causes, runs the two lint rules that find it, and sets up a zone handler for the errors that still slip through.
In this part:
- A future nobody awaits escapes every try
- Two lint rules find the missing await
- runZonedGuarded gives stray errors one handler
1 diagram · 4 runnable examples (Dart, Python) · about 6 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise
Check the three ideas of the sections above, async functions, a try around an await and parallel waits, in a short session that explains every answer:
Practice that adapts to you
Practise this lesson
7 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What does an async function return, and when does its body start running?
It returns a future, always: Future<T> for a function that returns a T, and Future<void> when it returns
nothing. Its body starts at once, during the call, and runs synchronously up to its first await. There it stops, and
the caller receives the future, which has not completed yet. The rest of the body runs later, when the awaited future
completes, and the function’s return value then completes the caller’s future. In async_flow.dart, fetchPrice
printed its first line before main received the Future<int>.
Three longer questions take these ideas into failures and deadlines:
-
Mid to senior
A product page loads price, stock and reviews in parallel with Future.wait. The reviews service fails about 2 % of the time, and each time the whole page shows an error although price and stock arrived. Redesign the waiting so the page shows what arrived, and say what the code should log.
-
Mid to senior
After a refactor, a command line tool sometimes ends with Unhandled exception and exit code 255 a moment after it prints saved. The trace points at an analytics upload. Find the cause, fix it so a failed upload never ends the command, and stop the same bug from coming back.
-
Senior to staff
A payment-status call usually answers in 300 ms but sometimes hangs for 30 seconds. Add a 2-second deadline and up to three attempts with backoff. Say what happens to the calls that hang, the longest a user can wait, and which failures you retry.
Flashcards · 5 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does an async function return, and when does its body run?
Always a future. Its body starts at once, inside the call, runs synchronously up to its first await and then hands the caller a future that completes when the body finishes.
-
Does an await block the thread?
No. It hands control back to the event loop and resumes the function when the awaited future completes.
-
How do you catch the error of an awaited future?
With an ordinary try: when the awaited future fails, the await throws its error on that line, so try, on, catch and finally work as they do for synchronous code.
-
Two independent 300 ms calls: how do you wait for both in about 300 ms instead of 600?
Start both first, put the futures in a record and await its .wait; the result is a record of values that a pattern unpacks. Two awaits in a row run the calls one after the other. Future.wait does the same for a list of one type.
-
What happens when an awaited async call throws and no caller up to main catches it?
The error reaches main at its await, and with no try there the program ends with exit code 255.
Key takeaways
- An async function runs at once up to its first await, returns a future to its caller and finishes when the awaited future completes.
- An await throws the error of a future that failed, so
try,on,catchandfinallyhandle asynchronous errors like synchronous ones. - Two awaits in a row run the calls one after the other; starting both and awaiting a record’s
.waitruns them together, in about the time of the slower one.
Terms in this lesson
- async function
- A function marked async. It may use await, always returns a future, runs synchronously up to its first await and finishes later, when the futures it awaits have completed.
- lost future
- A future that no code awaits and no handler is attached to, so its result goes nowhere and its error cannot be caught by the code that started it.
- zone
- A dart:async context in which code and the callbacks it schedules run. A zone can bring its own handler for asynchronous errors that nothing else handles.
References
- Asynchronous programming: futures, async, await (The Dart project (Google))
- Futures and error handling (The Dart project (Google))
- Asynchronous programming (language tour) (The Dart project (Google))
- ParallelWaitError class (The Dart project (Google))
- FutureRecord2.wait property (The Dart project (Google))
- Future.wait static method (The Dart project (Google))
- Future.timeout method (The Dart project (Google))
- runZonedGuarded function (The Dart project (Google))
- unawaited function (The Dart project (Google))
- unawaited_futures lint rule (The Dart project (Google))
- discarded_futures lint rule (The Dart project (Google))
- async_return_with_no_await lint rule (The Dart project (Google))
- Dart SDK changelog (3.0.0, 3.4.0, 3.9.0, 3.10.0, 3.13.0) (The Dart project (Google))
- Future.ignore method (The Dart project (Google))
Report a problem with this lesson
Kept only in this browser. Your Learn progress