ZetCode

Dart Future

last modified May 29, 2026

In this article we show how to work with futures in Dart language.

Future

A Future represents a potential value, or error, that will be available at some time in the future.

A Future can be complete with a value or with an error. Programmers can plug callbacks for each case. Futures and async and await keywords are used to perform asynchronous operations in Dart.

Common asynchronous operations include:

The async and await keywords provide a declarative way to define asynchronous functions and use their results. We mark an asynchronous function with the async keyword. The await keyword is used to get the completed result of an asynchronous expression. The await keyword only works within an asynchronous function.

Some languages use the term Promise for the same functionality.

Dart future simple example

The Future.value creates a future completed with value.

main.dart
void main() async {
  final myfut1 = Future.value(14);
  print(myfut1);

  final myfut2 = await Future.value(14);
  print(myfut2);
}

The example contrasts a Future printed without await against one resolved with await.

void main() async {

The main function is marked async because it uses the await keyword inside.

final myfut1 = Future.value(14);
print(myfut1);

Without await, myfut1 holds the unresolved Future<int> object itself, not the integer value. Printing it outputs the type description rather than 14.

var myfut2 = await Future.value(14);
print(myfut2);

With await, execution suspends until the future completes and myfut2 receives the resolved value 14.

$ dart main.dart
Instance of 'Future<int>'
14  

Dart Future.delayed

The Future.delayed creates a future that runs its computation after a delay.

main.dart
void main() async {
  Future.delayed(Duration(seconds: 2), () => 12).then((value) => print(value));

  final res = await Future.delayed(Duration(seconds: 2), () => 14);
  print(res);
}

The example shows two ways to consume a delayed future: the callback style with then and the async/await style.

Future.delayed(Duration(seconds: 2), () => 12).then((value) => print(value));

Future.delayed schedules the computation () => 12 after a two-second delay. The chained then registers a callback that runs once the future completes, printing the resolved value.

final res = await Future.delayed(Duration(seconds: 2), () => 14);
print(res);

The await keyword suspends the function until the delayed future resolves, then assigns the result to res. Both futures start at roughly the same time, so the total wait is about two seconds, not four.

$ dart main.dart
12
14

Dart read file with readAsString

The readAsString function reads the entire file contents as a string asynchronously. It returns a Future<String> that completes with the string once the file contents has been read.

words.txt
bear
fruit
cloud
sky
forest
falcon
wood
lake
rock

This is the words.txt file.

main.dart
import 'dart:io';

void main() async {
  final file = File('words.txt');

  final contents = await file.readAsString();
  print(contents);
}

The example reads the words.txt file asynchronously and prints its full content. The await suspends main until the read completes, after which the string is available in contents.

Dart Future.wait

The Future.wait waits for multiple futures to complete and collects their results. It returns a future which completes once all the given futures have completed.

main.dart
import 'dart:math';

Future<int> getRandomValue() async {
  await Future.delayed(Duration(seconds: 1));
  final random = Random();
  return random.nextInt(150);
}

int findMaxVal(List<int> lst) {
  lst.forEach((e) => print(e));

  return lst.reduce(max);
}

void main() async {
  final maximum = await Future.wait([
    getRandomValue(),
    getRandomValue(),
    getRandomValue(),
    getRandomValue(),
    getRandomValue(),
    getRandomValue()
  ]).then((List<int> results) => findMaxVal(results));

  print('Maximum is : $maximum');
}

In the example, getRandomValue simulates an async operation by waiting one second before returning a random integer. We launch it six times and pass all futures to Future.wait, which resolves only after every future completes. The then callback receives the collected results and finds the maximum value.

$ dart main.dart
94
13
106
41
110
122
Maximum is : 122

In the following example we check for the existence of a directory.

main.dart
import 'dart:io';

void main() async {
  final path = 'doc/crypto';
  final dr = Directory(path);

  final present = await dr.exists();

  if (present) {
    print('directory exists');
    Future.wait([dr.delete()]).then((_) => checkIfExists(dr));
  } else {
    print('directory does not exist');
  }
}

void checkIfExists(Directory dr) async {
  final present = await dr.exists();
  print(present ? 'directory exists' : 'directory does not exist');
}

If the given directory exists, we delete it, then check for its existence again.

Future.wait([dr.delete()]).then((_) => checkIfExists(dr));

With Future.wait we wait for the directory to be deleted. Only after the asynchronous operation finishes, we call checkIfExists.

Dart future get request

The http is a composable, Future-based library for making HTTP requests.

$ dart pub add http

We add the http package.

pubspec.yaml
name: app

environment:
    sdk: '>=3.0.0 <4.0.0'
dependencies:
    http: ^1.2.0

This is the pubspec.yaml.

main.dart
import 'package:http/http.dart' as http;

Future<String> fetchData() async {
  final resp = await http.get(Uri.http('webcode.me'));

  if (resp.statusCode == 200) {
    return resp.body;
  } else {
    throw Exception('Failed to fetch data');
  }
}

void main() async {
  final data = await fetchData();
  print(data);
}

In the example, we send a GET request to the webcode.me and print the response body.

$ dart main.dart
<!DOCTYPE html>
<html lang="en">
<head>
...

Dart Future error handling

Errors thrown inside a future propagate through the chain, bypassing then callbacks until a catchError handler is reached. With async/await, a standard try/catch block handles future errors instead.

main.dart
void main() async {
  Future(() => throw 'something went wrong')
      .then((_) => print('never reached'))
      .catchError((e) => print('caught: $e'));
}

The error thrown inside the future skips the then callback entirely and is delivered to catchError.

$ dart main.dart
caught: something went wrong

The equivalent code using async/await with a try/catch block:

main.dart
Future<int> divide(int a, int b) async {
  if (b == 0) throw ArgumentError('division by zero');
  return a ~/ b;
}

void main() async {
  try {
    final result = await divide(10, 0);
    print(result);
  } on ArgumentError catch (e) {
    print('caught: $e');
  }
}

Using try/catch with await is generally preferred over catchError because it supports typed error handling with on clauses and reads as linear, synchronous code.

$ dart main.dart
caught: Invalid argument(s): division by zero

Dart Future chaining

Futures can be chained with then, where each callback receives the result of the previous future and may return a new one. The async/await style expresses the same logic in a linear, easier-to-read form.

main.dart
Future<String> fetchUser(int id) async {
  await Future.delayed(Duration(milliseconds: 100));
  return 'user_$id';
}

Future<String> fetchRole(String user) async {
  await Future.delayed(Duration(milliseconds: 100));
  return '$user: admin';
}

void main() async {
  // chaining style
  await fetchUser(1)
      .then((user) => fetchRole(user))
      .then(print);

  // async/await style
  final user = await fetchUser(2);
  final role = await fetchRole(user);
  print(role);
}

Both styles produce the same result. Chaining is concise for short pipelines, while async/await is clearer when the logic grows or requires intermediate variables and error handling.

$ dart main.dart
user_1: admin
user_2: admin

Dart Future.any

Future.any accepts a list of futures and completes with the result of whichever future finishes first. The remaining futures continue running but their results are discarded.

main.dart
Future<String> fetchFrom(String name, int ms) async {
  await Future.delayed(Duration(milliseconds: ms));
  return 'response from $name';
}

void main() async {
  final result = await Future.any([
    fetchFrom('server A', 400),
    fetchFrom('server B', 150),
    fetchFrom('server C', 600),
  ]);

  print(result);
}

Three requests are issued in parallel. Future.any resolves as soon as the fastest one completes — here server B after 150 ms. This pattern is useful for redundant requests where only the fastest response matters. If all futures fail, Future.any completes with the last error.

$ dart main.dart
response from server B

Dart Future timeout

The timeout method enforces a time limit on a future. If the future does not complete within the given duration, a TimeoutException is thrown. An optional onTimeout callback can return a fallback value instead.

main.dart
import 'dart:async';

Future<String> slowOperation() async {
  await Future.delayed(Duration(seconds: 5));
  return 'done';
}

void main() async {
  try {
    final result = await slowOperation()
        .timeout(Duration(seconds: 2));
    print(result);
  } on TimeoutException {
    print('operation timed out');
  }
}

slowOperation takes five seconds to complete, but the two-second timeout fires first. The TimeoutException is caught by the try block.

$ dart main.dart
operation timed out

Dart Completer

A Completer gives manual control over a future's completion. You create a completer, expose its future property to callers, and resolve or reject it at any point with complete or completeError. This is useful for bridging callback-based APIs into the future model.

main.dart
import 'dart:async';

Future<int> computeValue() {
  final completer = Completer<int>();

  Future.delayed(Duration(seconds: 1), () {
    completer.complete(42);
  });

  return completer.future;
}

void main() async {
  final value = await computeValue();
  print('computed: $value');
}
final completer = Completer<int>();

A Completer<int> is created. Its future property is an unresolved Future<int> that callers can await.

completer.complete(42);

After one second the completer is resolved with 42, waking any awaiting code. Use completer.completeError(e) to reject the future with an error. A completer may only be completed once; calling complete or completeError a second time throws a StateError.

$ dart main.dart
computed: 42

Dart microtask queue

Dart's event loop uses two queues: the microtask queue and the event queue. Microtasks have higher priority — they drain completely before the event loop picks up the next event. Futures created with Future() place their callbacks on the event queue, while scheduleMicrotask and the continuations of async/await expressions go onto the microtask queue.

The execution order is:

main.dart
import 'dart:async';

void main() {
  print('A');

  Future(() => print('B'));            // event queue
  scheduleMicrotask(() => print('C')); // microtask queue

  print('D');
}
print('A');
...
print('D');

Synchronous statements run first, so A and D are printed before any async work starts.

scheduleMicrotask(() => print('C'));

After the synchronous code finishes, the microtask queue drains. The scheduleMicrotask callback runs and prints C.

Future(() => print('B'));

Only after all microtasks are processed does the event loop take the next item from the event queue, printing B last.

$ dart main.dart
A
D
C
B

Source

Dart Future - language reference

In this article we have worked with futures in Dart.

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.