ZetCode

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.

async_basics.dart
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.

async_values.dart
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.

sequential_concurrent.dart
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.

async_errors.dart
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.

await_for.dart
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.

async_timeout.dart
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.