Skip to content

Dart Async Programming — Futures, Async, and Await

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Dart Async Programming. We cover key concepts, practical examples, and best practices to help you master this topic.

Dart async programming uses Future objects and async/await syntax to perform non-blocking operations, allowing applications to remain responsive while waiting for I/O, network requests, or timers.

What You Will Learn

  • Understanding asynchronous programming and the event loop
  • Creating and using Future objects
  • The async and await keywords
  • Error handling in async code
  • Running operations concurrently with Future.wait
  • Delaying execution with Future.delayed
  • Converting callbacks to async/await

Why It Matters

Most real-world applications need to perform operations that take time: HTTP requests, database queries, file I/O, or timers. Without async programming, these operations block the entire application, causing UI freezes in Flutter and unresponsive servers. Dart's async model is based on a single-threaded event loop with futures and streams. Understanding this model is critical because Flutter uses it for every user interaction, animation, and network call.

Real-World Use

The DodaTech Flutter app uses async/await for every network request. When a user searches for a course, the app awaits the API response while showing a loading indicator. The UI remains responsive during the wait. All database operations with SQLite use async methods, and file downloads show progress using Streams.

Learning Path

flowchart LR
  A[Dart Generics] --> B[Async Programming\nYou are here]
  B --> C[Dart Streams]
  style B fill:#f90,color:#fff

The Event Loop

Dart runs on a single thread with an event loop. Synchronous code runs immediately. Asynchronous operations are scheduled to run later:

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

  Future(() => print('Future 1'));
  Future(() => print('Future 2'));

  print('End');
}

Output:

Start
End
Future 1
Future 2

Even though Future 1 and Future 2 are created before End is printed, they execute after the current synchronous block completes. The event loop processes them when the main code finishes.

Creating a Future

A Future represents a value that will be available at some point in the future:

void main() {
  print('Before future');

  var future = Future<String>(() {
    print('Inside future (async)');
    return 'Hello from future';
  });

  future.then((value) {
    print('Future completed with: $value');
  });

  print('After future');
}

Output:

Before future
After future
Inside future (async)
Future completed with: Hello from future

The callback passed to Future() runs asynchronously. The .then() method registers a callback that runs when the future completes. The main code continues without waiting.

Async and Await

The async and await keywords make asynchronous code look synchronous:

Future<String> fetchUserData() {
  return Future.delayed(Duration(seconds: 2), () {
    return 'User: Alice';
  });
}

Future<void> main() async {
  print('Fetching user data...');

  var data = await fetchUserData();

  print('Data received: $data');
  print('Done');
}

Output:

Fetching user data...
(2 second pause)
Data received: User: Alice
Done

The await keyword pauses the execution of the main function until the future completes. The function must be marked async. While main is awaiting, the event loop can Process other events, so the application remains responsive.

Error Handling

Errors in async functions are caught with try-catch:

Future<String> fetchFromApi(bool shouldFail) {
  return Future.delayed(Duration(seconds: 1), () {
    if (shouldFail) {
      throw Exception('Network error');
    }
    return 'Success: data loaded';
  });
}

Future<void> main() async {
  try {
    var result = await fetchFromApi(true);
    print(result);
  } catch (e) {
    print('Error caught: $e');
  }

  // Using .catchError()
  await fetchFromApi(true)
      .then((value) => print(value))
      .catchError((error) => print('Caught: $error'));

  print('Program continues');
}

Output:

Error caught: Exception: Network error
Caught: Exception: Network error
Program continues

Use try-catch with async/await for familiar error handling. The .catchError() method is used with the .then() chain style. Unhandled errors in futures cause unhandled exception events.

Async/Await with Multiple Operations

Await multiple futures sequentially or concurrently:

Future<String> fetchUser(int id) async {
  await Future.delayed(Duration(seconds: 1));
  return 'User $id';
}

Future<String> fetchOrders(String user) async {
  await Future.delayed(Duration(seconds: 1));
  return 'Orders for $user';
}

Future<void> main() async {
  // Sequential: total 2 seconds
  print('Sequential execution:');
  var start = DateTime.now();

  var user = await fetchUser(1);
  var orders = await fetchOrders(user);

  print('${DateTime.now().difference(start).inSeconds}s: $orders');

  // Concurrent: total 1 second
  print('\nConcurrent execution:');
  start = DateTime.now();

  var results = await Future.wait([
    fetchUser(1),
    fetchOrders('User 1'),
  ]);

  print('${DateTime.now().difference(start).inSeconds}s: $results');
}

Output:

Sequential execution:
(1 second pause)
(1 second pause)
2s: Orders for User 1

Concurrent execution:
(1 second pause)
1s: [User 1, Orders for User 1]

Use Future.wait() when operations are independent and can execute in parallel. Sequential awaiting is appropriate when later operations depend on the results of earlier ones.

Future.delayed and Future.value

Dart provides utility constructors for common Future patterns:

void main() async {
  // Future.delayed: wait before completing
  print('Waiting 1 second...');
  await Future.delayed(Duration(seconds: 1));
  print('Done waiting');

  // Future.value: immediately complete with a value
  var immediate = Future.value(42);
  print('Immediate: ${await immediate}');

  // Future.error: immediately complete with an error
  var error = Future.error('Something went wrong');
  try {
    await error;
  } catch (e) {
    print('Error result: $e');
  }

  // Future.microtask: schedule on microtask queue
  Future.microtask(() => print('Microtask runs'));
  print('This prints first');
}

Output:

Waiting 1 second...
(1 second pause)
Done waiting
Immediate: 42
Error result: Something went wrong
This prints first
Microtask runs

Future.microtask schedules a task on the microtask queue, which runs before the event queue. Use it for short operations that should complete before the next frame.

Converting Callbacks to Futures

Older APIs use callbacks. Convert them to Futures for cleaner code:

import 'dart:io';

// Simulating a callback-based API
class LegacyApi {
  void fetchData(void Function(String data) onSuccess, void Function(String error) onError) {
    // Simulate work
    Future.delayed(Duration(seconds: 1), () {
      onSuccess('Legacy data loaded');
    });
  }
}

// Convert to Future-based API
Future<String> fetchDataAsync() {
  var api = LegacyApi();
  var completer = Completer<String>();

  api.fetchData(
    (data) => completer.complete(data),
    (error) => completer.completeError(error),
  );

  return completer.future;
}

Future<void> main() async {
  print('Fetching from legacy API...');

  try {
    var data = await fetchDataAsync();
    print('Got: $data');
  } catch (e) {
    print('Error: $e');
  }
}

Output:

Fetching from legacy API...
(1 second pause)
Got: Legacy data loaded

Completer<T> creates a Future that you can complete manually. Call completer.complete(value) on success or completer.completeError(error) on failure.

Timeouts

Use .timeout() to set a maximum wait time for a Future:

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

Future<void> main() async {
  print('Starting operation...');

  try {
    var result = await slowOperation().timeout(
      Duration(seconds: 3),
      onTimeout: () => 'Timeout fallback',
    );
    print('Result: $result');
  } catch (e) {
    print('Operation failed: $e');
  }
}

Output:

Starting operation...
(3 second pause)
Result: Timeout fallback

Without onTimeout, a timeout throws TimeoutException. With onTimeout, the provided value or Future is used instead. This is useful for providing degraded experiences when services are slow.

Async Streams

Streams are the async version of Iterables. They provide a sequence of values over time:

import 'dart:async';

Stream<int> countStream(int max) async* {
  for (int i = 1; i <= max; i++) {
    await Future.delayed(Duration(milliseconds: 500));
    yield i;
  }
}

Future<void> main() async {
  print('Counting...');

  await for (var value in countStream(5)) {
    print('Count: $value');
  }

  print('Done counting');
}

Output:

Counting...
Count: 1
(500ms pause)
Count: 2
(500ms pause)
Count: 3
(500ms pause)
Count: 4
(500ms pause)
Count: 5
Done counting

The async* keyword marks a function as an asynchronous generator. yield emits values one at a time. The await for loop consumes the stream, processing each value as it arrives.

Common Mistakes

  1. Forgetting to await a Future: Without await, the function continues immediately and returns a Future instead of the value. The compiler warns about unused futures, but it is easy to miss.

  2. Blocking the event loop with synchronous operations: Future.delayed and await do not block. But calling sleep() or doing CPU-intensive work in a Future callback blocks the event loop. Use Isolate for CPU-heavy tasks.

  3. Not handling errors in async functions: An unhandled error in an async function creates an unhandled Future that may crash the application. Always wrap async code in try-catch.

  4. Using .then() when await is clearer: .then() chains can create deeply nested code. Prefer await for readability, but .then() can be useful for simple transformations.

  5. Creating Future constructors that execute immediately: The callback passed to Future() runs asynchronously, not immediately. If you need immediate execution, call the function directly without wrapping in Future.

Practice Questions

  1. How does the Dart event loop process synchronous and asynchronous code?
  2. What is the difference between Future.wait and awaiting futures sequentially?
  3. How does Completer help bridge callback-based APIs to Future-based APIs?
  4. What happens when a Future times out?
  5. Challenge: Write a function that retries an asynchronous operation up to 3 times with exponential backoff (500ms, 1s, 2s delays). The function should return the result on success or throw after all retries fail.

Mini Project

Build an asynchronous data loading system:

  • Implement fetchUser(int id) that simulates a 1-second API call
  • Implement fetchPosts(int userId) that simulates a 1.5-second API call
  • Implement fetchComments(int postId) that simulates a 0.5-second API call
  • Load user, posts, and comments concurrently using Future.wait
  • Add timeout handling for slow responses
  • Add retry logic for failed requests
  • Measure and print execution time

FAQ

Is Dart's async model single-threaded?

Yes, Dart uses a single-threaded event loop. Async operations do not create new threads. They schedule work on the event queue and resume when the operation completes.

How do I run CPU-intensive code without blocking the event loop?

Use Isolate.spawn() to run code in a separate isolate. Each isolate has its own memory heap and event loop, enabling parallel execution on multi-core CPUs.

What is the difference between `Future` and `Stream`?

A Future completes once with a single value. A Stream provides multiple values over time. Use Future for one-shot operations (HTTP request). Use Stream for continuous data (user input events, file reading).

Can I cancel a running Future?

Dart does not have built-in Future cancellation. Use a CancelableOperation from the async package or implement cancellation with a boolean flag checked periodically.

How does Flutter handle async operations during widget build?

Flutter calls setState() inside the async callback to trigger a rebuild when the async operation completes. The FutureBuilder widget provides a declarative way to handle async state.

What is Next

Now that you understand async programming, learn about streams for continuous data. Proceed to Dart Streams for stream controllers, transformations, and subscriptions. Then explore Dart Isolates for parallel execution.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro