Async Basics and Futures

By Syncfusion®, Inc. · Last updated

How Dart runs slow work without freezing: the event loop, Futures and their states, then and catchError, async and await, and waiting for several Futures with wait, any and timeout.

What Is Asynchronous Programming?

  • Synchronous (sync) code runs one line after another. Each line waits for the one before to finish.
  • Asynchronous (async) code can start a slow job, move on, and come back when the job is done.
  • Blocking = the program stops and waits, doing nothing else.
  • Non-blocking = the program keeps working while the slow job runs.

Simple lesson: async code starts slow work, keeps going, and picks up the result later.

A Real-World Example

Think of ordering food at the school canteen.

  • Sync way: you stand at the counter until your dosa is ready. The whole line behind you waits.
  • Async way: you get a token, sit down, and chat with friends. When your number is called, you collect the dosa.
  • The token is a promise of food later. In Dart, that token is called a Future.

Every Run box on this page runs on the course's Dart runner in your browser.

The runner fast-forwards waiting time. A 3-second delay finishes at once, but always in the right order.

What You Will Learn

In this lesson, you will learn:

  1. How Dart's event loop decides what runs next.
  2. What a Future is, and its states.
  3. How to create Futures, and handle them with then, catchError and whenComplete.
  4. How async and await make async code read like normal code.
  5. How to wait for many Futures, and how to give up with timeout.

The Event Loop

One Thing at a Time

Dart runs your code on one thread. It never runs two lines at the same moment. To handle slow jobs, it uses an event loop.

  • Event loop = a loop that keeps taking the next job and running it.
  • Event queue = jobs like timers, finished downloads, and Future(...) work.
  • Microtask queue = small, urgent jobs. They always run before the next event.

The Order of Things

Step through this program and watch the queues:

Sync first, then microtasks, then events

import 'dart:async';

void main() {
  print('1. main starts');
  Future(() => print('5. event queue'));
  scheduleMicrotask(() => print('3. microtask'));
  Future.microtask(() => print('4. another microtask'));
  print('2. main ends');
}
  • main runs to the end first: 1, 2.
  • Then all microtasks: 3, 4.
  • Then the event queue: 5.

A long loop in your code blocks the event loop. While it runs, no Future can finish and the app cannot react to taps.

Keep each job short. For heavy work, use an isolate (see Isolates).

Simple lesson: Dart finishes the current code, then empties the microtask queue, then takes the next event.

Futures

What Is a Future?

Future = an object that will hold a value later. Future<int> will give an int later.

A Future is always in one of these states:

StateMeansCanteen token
Uncompletedthe work is still goingdosa is cooking
Completed with a valuethe work finished well"Number 42, your dosa!"
Completed with an errorthe work failed"Sorry, no dosa batter"

Creating Futures

CodeGivesUse
Future(() => work())runs work later, from the event queuestart work later
Future.value(5)a Future already holding 5a ready answer
Future.delayed(Duration(seconds: 2), () => 5)5 after 2 secondswaiting, timers, demos
Future.error(StateError('no batter'))a Future that failstesting error paths

A Future that finishes after a delay

Future<String> cookDosa() {
  return Future.delayed(Duration(seconds: 3), () => 'Masala dosa');
}

void main() {
  print('Order placed');
  cookDosa().then((food) => print('Got: $food'));
  print('Chatting with friends...');
}

Chatting with friends... prints before Got: Masala dosa. main did not wait.

Simple lesson: a Future is a token for a value that is not ready yet.

then, catchError and whenComplete

MethodRuns whenLike
.then((value) { ... })the Future gives a valuetry
.catchError((e) { ... })the Future failscatch
.whenComplete(() { ... })either wayfinally

Handling success, failure and both

Future<int> fetchMarks(String name) {
  return Future.delayed(Duration(seconds: 1), () {
    if (name == 'Unknown') {
      throw StateError('No such student');
    }
    return 88;
  });
}

void main() {
  fetchMarks('Asha')
      .then((m) => print('Asha: $m'))
      .whenComplete(() => print('First request done'));

  fetchMarks('Unknown')
      .then((m) => print('Marks: $m'))
      .catchError((e) => print('Failed: ${e.message}'))
      .whenComplete(() => print('Second request done'));
}

Chaining

Each then returns a new Future, so you can chain steps:

A chain of then steps

void main() {
  Future.value(40)
      .then((marks) => marks + 5)
      .then((marks) => marks * 2)
      .then((total) => print('Final score: $total'));
}

Simple lesson: then handles the value, catchError handles failure, and whenComplete runs either way.

async and await

The Easy Way

Long then chains are hard to read. async and await let you write the same thing like normal code:

  • async after the parameter list marks a function as async. It always returns a Future.
  • await waits for a Future inside an async function, and gives you its value.
  • While it waits, the event loop keeps running other jobs. Nothing freezes.

await pauses this function only

Refactoring then into await

The same steps, two styles

Future<int> getMarks() => Future.delayed(Duration(seconds: 1), () => 72);
Future<int> getBonus() => Future.delayed(Duration(seconds: 1), () => 8);

void withThen() {
  getMarks().then((m) {
    getBonus().then((b) {
      print('then style: ${m + b}');
    });
  });
}

Future<void> withAwait() async {
  var m = await getMarks();
  var b = await getBonus();
  print('await style: ${m + b}');
}

Future<void> main() async {
  withThen();
  await withAwait();
}

try and catch in Async Code

With await, a failed Future throws at that line. So a normal try works (see Exception Handling):

Catching a failed Future with try

Future<int> fetchFee(String name) async {
  await Future.delayed(Duration(seconds: 1));
  if (name.isEmpty) {
    throw ArgumentError('Name is empty');
  }
  return 1200;
}

Future<void> main() async {
  try {
    var fee = await fetchFee('');
    print('Fee: $fee');
  } on ArgumentError catch (e) {
    print('Could not fetch: ${e.message}');
  } finally {
    print('Request finished');
  }
}

await only works inside a function marked async.

Forgetting await is a common bug: you get the Future itself, not its value. The next line runs too early.

Simple lesson: mark the function async, put await before each Future, and use try for errors.

Many Futures at Once

Future.wait

Future.wait([...]) waits for all Futures, and gives a list of their values, in order.

Three requests, waited for together

Future<int> marks(String subject, int value, int seconds) =>
    Future.delayed(Duration(seconds: seconds), () => value);

Future<void> main() async {
  var all = await Future.wait([
    marks('Maths', 92, 3),
    marks('Science', 85, 1),
    marks('English', 78, 2),
  ]);
  print(all);
  print('Total: ${all.reduce((a, b) => a + b)}');
}
  • The three run at the same time, so the total wait is 3 seconds, not 6.
  • The list keeps the order you asked, not the order they finished.

Future.any and timeout

  • Future.any([...]) gives the value of the first Future to finish.
  • .timeout(duration) fails with a TimeoutException if the Future takes too long.

The fastest server, and giving up

import 'dart:async';

Future<String> server(String name, int seconds) =>
    Future.delayed(Duration(seconds: seconds), () => name);

Future<void> main() async {
  var fastest = await Future.any([server('Mumbai', 3), server('Chennai', 1)]);
  print('Fastest: $fastest');

  try {
    var slow = await server('Delhi', 10).timeout(Duration(seconds: 2));
    print(slow);
  } on TimeoutException {
    print('Delhi took too long. Showing saved data.');
  }
}

TimeoutException comes from dart:async, so that import is needed.

Easy way to remember

  • Future = a token for "later".
  • then / catchError / whenComplete = try / catch / finally for tokens.
  • async + await = wait for the token, but let others keep working.
  • wait = all of them. any = the first. timeout = not forever.

Simple lesson: wait for all, any for the fastest, and timeout so you never wait forever.

TL;DR

  1. Async = start slow work, keep going, use the result later. Dart uses one thread and an event loop.
  2. Order: current code, then microtasks, then events.
  3. Future = a value later: uncompleted, completed with value, or with error.

    • Make one with Future(), .value, .delayed, .error.
  4. then, catchError, whenComplete, or the easier async / await with try.
  5. Future.wait (all), Future.any (first), .timeout (give up, TimeoutException).

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Forgetting awaitThe call gives the valueIt gives a FutureThe next line uses a Future, not the value.
await in a function without asyncawait works anywhereMark the function asyncA compile error.
A long loop in an async appAsync means parallelDart runs one job at a timeThe app freezes.
Awaiting one by one when they are independentOrder needs awaitUse Future.waitIt takes much longer.
No catchError or tryFutures cannot failA failed Future throwsUnhandled exception.
Expecting Future(...) to run at onceIt runs immediatelyIt runs later, from the event queueOutput order surprises you.

Check Yourself

Exercises

What to Remember

  • Async code starts slow work and keeps going. Dart runs one job at a time with an event loop.
  • Current code runs first, then microtasks, then events.
  • A Future is a value that comes later, or an error.
  • Use async and await with try for clear async code.
  • Future.wait waits for all, Future.any takes the first, and timeout stops waiting.

Next: Streams. You will handle many values that arrive one after another.