Dart async and await
last modified October 18, 2026
In this article we explore async and await in Dart, the syntax that lets asynchronous code read almost like synchronous code while the event loop keeps running other work.
async and await Overview
Dart runs on a single-threaded event loop. When a piece of code waits for slow work such as a file read or a network call, it does not block the whole program; instead it yields and lets other tasks run. The tools for this are Future, which represents a value that will arrive later, and async/await, which make working with futures readable.
Marking a function async makes it return a Future. Inside
such a function, await pauses until the awaited future completes and
then produces its value. Crucially, the pause is not a busy wait: control returns
to the event loop, so other events are processed while the operation is pending.
| Construct | Meaning | Example |
|---|---|---|
async |
Function returns a Future |
Future<int> f() async { ... } |
await |
Wait for a future and unwrap its value | final x = await f(); |
Future<T> |
Promise of a value of type T |
Future<String> |
await for |
Consume a Stream asynchronously |
await for (final e in stream) |
async main |
Allow top-level code to await | Future<void> main() async |
async and await Basics
An async function that returns a value declares the value type inside
a Future. Callers cannot read that value directly; they must
await the future it returns.
Future<String> fetchUser() async {
await Future.delayed(const Duration(milliseconds: 300));
return 'Alice';
}
Future<void> main() async {
print('start');
final user = await fetchUser();
print('got $user');
print('end');
}
Despite the 300 millisecond delay, the three lines print in order. The
await suspends main until fetchUser
finishes, then resumes with the returned value bound to user. Making
main return Future<void> allows top-level code to
use await.
$ dart run async_basics.dart start got Alice end
Returning Values from Async Functions
An async function always returns a Future, even for a
small computation. The returned value is wrapped, and await unwraps
it. Because await itself has high precedence, several awaits can be
combined in one expression.
Future<int> square(int n) async {
await Future.delayed(const Duration(milliseconds: 20));
return n * n;
}
Future<void> main() async {
Future<int> pending = square(5); // an async function returns a Future
final value = await pending;
print(value);
final total = await square(2) + await square(3);
print(total);
}
square computes n * n but its return type is
Future<int>, so pending is a future rather than a
number. Awaiting it yields 25. In the last line the two calls are
each awaited before the addition, producing 4 + 9.
$ dart run async_values.dart 25 13
Sequential versus Concurrent Awaiting
Where you place await controls whether operations run one after
another or overlap. Calling these asynchronous functions starts their operations;
awaiting each call before making the next one makes them sequential. Starting both
operations before awaiting their combined result lets them overlap.
Future<int> task(int ms) async {
await Future.delayed(Duration(milliseconds: ms));
return ms;
}
Future<void> main() async {
final sw1 = Stopwatch()..start();
await task(100);
await task(100);
final seq = sw1.elapsedMilliseconds;
final sw2 = Stopwatch()..start();
final a = task(100);
final b = task(100);
await Future.wait([a, b]);
final concurrent = sw2.elapsedMilliseconds;
print('sequential >= 200: ${seq >= 200}');
print('concurrent < sequential: ${concurrent < seq}');
}
In the first block each await waits for the previous task, so two
100 millisecond tasks take at least 200 milliseconds. In the second block both
tasks are started before their combined result is awaited, so they overlap and the
total is close to 100 milliseconds. Future.wait attaches handlers to
both futures immediately. Awaiting them separately could leave an error from the
second future unhandled while the first is pending. Wrap the combined await in
try/catch when failures need to be handled.
$ dart run sequential_concurrent.dart sequential >= 200: true concurrent < sequential: true
Error Handling in Async Code
An exception thrown inside an async function completes its future
with an error. That error surfaces at the await, where ordinary
try, catch, and finally work exactly as
they do in synchronous code.
Future<int> risky(bool ok) async {
await Future.delayed(const Duration(milliseconds: 50));
if (!ok) throw StateError('boom');
return 42;
}
Future<void> main() async {
try {
print(await risky(true));
} catch (e) {
print('caught: $e');
}
try {
print(await risky(false));
} on StateError catch (e) {
print('caught StateError: ${e.message}');
} finally {
print('done');
}
}
The first call returns 42 and prints it. The second throws a
StateError, which the typed on StateError clause catches
and reports using its message. The finally block runs
either way, which makes it a reliable place to release resources.
$ dart run async_errors.dart 42 caught StateError: boom done
await for over Streams
A Future delivers a single value, but a Stream
delivers many values over time. The await for loop consumes a stream
asynchronously, pausing between events and resuming for each new value.
Stream<int> numbers() async* {
for (var i = 1; i <= 3; i++) {
await Future.delayed(const Duration(milliseconds: 20));
yield i;
}
}
Future<void> main() async {
await for (final n in numbers()) {
print('received $n');
}
print('stream closed');
}
The numbers function is an async generator, indicated by
async* and the yield keyword. The await for
loop receives 1, 2, and 3 as they are
produced, then finishes when the stream closes. This pattern is how async code
consumes streamed data such as lines read from a socket.
$ dart run await_for.dart received 1 received 2 received 3 stream closed
Timeouts
A future may take longer than a caller is willing to wait. The
timeout method returns a new future that completes with a
TimeoutException if the original does not finish in time.
import 'dart:async';
Future<String> slow() async {
await Future.delayed(const Duration(seconds: 2));
return 'done';
}
Future<void> main() async {
try {
final r = await slow().timeout(const Duration(milliseconds: 100));
print(r);
} on TimeoutException {
print('timed out');
}
}
slow would eventually return 'done', but the
timeout wrapper gives up after 100 milliseconds and throws a
TimeoutException, which the on TimeoutException clause
handles. Timeouts are a common way to keep asynchronous operations from hanging
indefinitely.
$ dart run async_timeout.dart timed out
Source
Dart Language: Asynchronous programming
Dart: Async and await
Dart API: dart:async
Dart Language: await for
async and await let asynchronous Dart read like ordinary
sequential code. Marking a function async wraps its result in a
future, await unwraps it, and the placement of await
determines whether work runs sequentially or concurrently. Error handling, stream
consumption with await for, and timeouts all build on the same model.
Author
List all Dart tutorials.