Dart: Asynchronous Functions

In Dart, asynchronous means something that doesn’t happen right away — it happens later. For example, waiting for a message, loading a file, or getting data from the internet. Dart uses three main tools for this: async, await, and Future. These help your program keep running while waiting. This is useful when you don’t want to stop everything just to wait for one thing to finish.

What is a Future?

A Future is like a promise. It means “I will give you something later.” It’s used when something takes time, like downloading a file or waiting for a result. Think of it like ordering a pizza — you order it now, but get it after some time.

Here’s a simple example:

Future<String> orderPizza() {
  return Future.delayed(Duration(seconds: 2), () => 'Pizza is here!');
}

This says: wait 2 seconds, then return the message. While waiting, your program can keep doing other things.

Using async and await

In Dart, you use async to mark a function that will do something later. Inside that function, you can use await to wait for a Future to finish.

Here’s a fun example:

Future<String> orderPizza() {
  return Future.delayed(Duration(seconds: 2), () => 'Pizza is here!');
}

void main() async {

  print('Ordering pizza...');
  String food = await orderPizza();

  print(food);

}

In this code, the main() function is marked with async, which means it can use the await keyword inside it. When the program calls await orderPizza(), it pauses and waits for the pizza to be ready. The orderPizza() function returns a Future that completes after 2 seconds. So, the program first prints “Ordering pizza…”, then waits, and finally prints “Pizza is here!” once the future is done.

Without async and await

If you don’t use async and await, Dart does not wait for the Future to finish. Instead, it moves on to the next line of code right away. This means you might try to use a value that hasn’t arrived yet.

void main() {

  print('Ordering pizza...');
  var food = orderPizza();

  print(food);

}

In this code, food will not be the pizza message. It will be a Future object because orderPizza() hasn’t finished yet. So the output will look like:

Ordering pizza...
Instance of 'Future<String>'

This shows why await is helpful — it waits for the real result instead of showing the Future itself.

Writing Your Own Async Function

You can create your own asynchronous function using async. This lets you pause the function while something is happening, like waiting for a delay or data. Here’s an example:

Future<String> sayHello() async {

  await Future.delayed(Duration(seconds: 1));
  return 'Hello!';

}

In this code, sayHello() waits one second using Future.delayed, then returns the word “Hello!”. Because the function takes time to finish, it returns a Future<String>, which means it will give a string later. You can use await to wait for this result when calling the function.

For example, you can call sayHello() like this:

void main() async {

  print('Getting ready...');

  String greeting = await sayHello();

  print(greeting);

}

This will first print “Getting ready…”, wait one second, then print “Hello!”. The await keyword pauses the code until sayHello() finishes, so everything runs in order.

Using .then() Instead of await

You don’t always need to use await. Dart also lets you use .then() to handle a Future. This means “when it’s done, do this.” But unlike await, the code after .then() does not wait — it runs right away.

Here’s an example:

Future<String> sayHello() async {

  await Future.delayed(Duration(seconds: 1));
  return 'Hello!';

}

void main() {

  print('Getting ready...');

  sayHello().then((greeting) {
    print(greeting);
  });

  print('This prints before the hello!');

}

When you use .then() instead of await, the function starts running in the background, and the next lines of code keep going right away. In the example, the program says “Getting ready…”, then sets up what to do after sayHello() finishes, and moves on. It prints “This prints before the hello!” right after. After one second, when the future is done, it finally prints “Hello!”. So, .then() doesn’t pause anything — it just schedules what to do later.

Fun Example: Making Breakfast

This example shows how to use multiple await calls to do tasks one after another. The makeBreakfast() function starts by frying eggs, which takes 2 seconds. It waits for this to finish before moving on to toast the bread, which takes 1 second. After both steps are done, it prints “Breakfast is ready!”.

Using await like this makes sure each step happens in order, just like in real life when you cook. Here’s the code:

Future<void> makeBreakfast() async {

  print('Frying eggs...');

  await Future.delayed(Duration(seconds: 2));

  print('Toasting bread...');

  await Future.delayed(Duration(seconds: 1));

  print('Breakfast is ready!');

}

void main() async {
  await makeBreakfast();
}

This shows how async functions let you write clear code for things that take time, making your program easier to understand.

Async Functions That Return Nothing

Sometimes, an async function doesn’t return any value. In these cases, use Future<void> as the return type. This tells Dart the function works asynchronously but does not send back data.

Here’s an example with a washDishes() function. It waits 1 second to simulate washing, then prints a message.

Future<void> washDishes() async {

  await Future.delayed(Duration(seconds: 1));
  print('Dishes are clean!');

}

void main() async {

  print('Starting chores...');

  await washDishes();

  print('All done!');

}

In this code, the program starts, waits for the dishes to be clean, then prints that everything is finished.

Running Multiple Futures Together

Sometimes, you want to run several tasks at the same time and wait for all to finish. Dart’s Future.wait lets you do this easily. It takes a list of futures and waits until every one of them is done.

Here’s an example running makeBreakfast() and washDishes() together. Both start at once, and the program waits for both before printing the final message.

Future<void> makeBreakfast() async {

  print('Frying eggs...');

  await Future.delayed(Duration(seconds: 2));

  print('Toasting bread...');

  await Future.delayed(Duration(seconds: 1));

  print('Breakfast is ready!');

}

Future<void> washDishes() async {

  await Future.delayed(Duration(seconds: 1));

  print('Dishes are clean!');

}

void main() async {

  await Future.wait([makeBreakfast(), washDishes()]);

  print('All done!');

}

This way, tasks run side by side, saving time while still keeping things clear and easy to follow.

Handling Errors

When working with async functions, errors can happen. You can use try and catch to handle these errors safely.

Here’s an example:

Future<String> fetchData() async {

  await Future.delayed(Duration(seconds: 1));
  throw Exception('Failed to fetch data');

}

void main() async {

  try {

    String data = await fetchData();
    print(data);

  } catch (e) {
    print('Error: $e');
  }

}

In this code, fetchData() throws an error after 1 second. The try block waits for the result, but when the error happens, the catch block runs and prints the error message. This way, your program won’t crash and can handle problems nicely.

Errors with Future.onError

You can catch errors from a future by using the onError method directly on the future. This lets you handle errors separately from the success case.

For example, here is a future that might fail. We catch the error with onError and print a message:

Future<String> riskyTask(bool fail) {

  return Future.delayed(Duration(seconds: 1), () {

    if (fail) throw Exception('Something went wrong');
    return 'Task completed';

  });

}

void main() {

  riskyTask(true).onError((error, stackTrace) {
    print('Error caught: $error');
    return 'Recovered from error';
  }).then((value) {
    print(value);
  });

}

If the future throws an error, onError catches it and returns a recovery value. Then the .then() prints that value. If there’s no error, it just prints the success result.

Errors with Future.catchError

catchError is another way to handle errors from a future. It lets you keep the success code in .then() and handle errors separately with catchError(). This helps make your code clear by separating what happens on success and what happens on error.

Here is an example where a future might fail. We use .then() for success and .catchError() for errors:

Future<String> riskyTask(bool fail) {

  return Future.delayed(Duration(seconds: 1), () {

    if (fail) throw Exception('Oops, an error happened!');
    return 'Task finished well!';

  });

}

void main() {

  riskyTask(true)
    .then((value) {
      print(value);
    })
    .catchError((error) {
      print('Caught error: $error');
    });

}

If the task fails, catchError catches the exception and prints a message. If it succeeds, the message in .then() prints.

Ignoring Results

Sometimes, a future’s result is not important, even if it has an error. For example, you might start a task but no longer care about its outcome. In Dart, you can use ignore() to safely run a future and completely ignore its result and any errors it may cause.

This means Dart handles all values or errors quietly so they don’t cause problems or warnings. This is different from leaving a future unawaited, which can cause errors to be missed or warnings to appear.

Here’s an example:

Future<void> cleanRoom() async {

  await Future.delayed(Duration(seconds: 2));
  print('Room is clean!');

}

void main() {

  cleanRoom().ignore();
  print('Started cleaning, but not waiting.');

}

Here, cleanRoom() runs in the background, and the program continues immediately without waiting or handling its result. If there are any errors in cleanRoom(), ignore() makes sure they don’t cause problems.

Here’s an example where the future throws an error:

Future<void> errorTask() async {

  await Future.delayed(Duration(seconds: 1));
  throw Exception('Something went wrong!');

}

void main() {

  errorTask().ignore();
  print('Started error task, but not waiting.');

}

In this code, errorTask() throws an error after 1 second. Because we used ignore(), the error is caught and ignored silently — the program keeps running without crashing or showing warnings.

If you want to silence warnings but still catch errors, you should use the unawaited function instead.

Handling Timeout

Sometimes a future might take too long to finish. You can use .timeout() to set a time limit. If the future does not complete in time, it throws a timeout error.

Here is an example where the future delays longer than the timeout:

Future<String> longTask() {
  return Future.delayed(Duration(seconds: 3), () => 'Done');
}

void main() async {

  try {
    String result = await longTask().timeout(Duration(seconds: 1));
    print(result);
  } catch (e) {
    print('Task timed out!');
  }

}

In this code, longTask() takes 3 seconds, but .timeout() only waits 1 second. After 1 second, the program throws a timeout error caught in the catch block, printing “Task timed out!”.

Using whenComplete

The whenComplete method lets you run some code after a future finishes, no matter if it worked or gave an error. It is useful for cleanup tasks or actions you want to always run.

For example, imagine a future that simulates a download. We use whenComplete to print a message when the process ends, whether it succeeds or fails:

Future<String> downloadFile(bool fail) {

  return Future.delayed(Duration(seconds: 2), () {
    if (fail) throw Exception('Download failed');
    return 'File downloaded';
  });

}

void main() {

  downloadFile(false)
    .whenComplete(() => print('Download attempt finished'))
    .then((value) => print(value))
    .catchError((error) => print(error));

}

In this code, the message “Download attempt finished” always prints after the future ends. Then, if the future worked, it prints the result; if it failed, it prints the error.

Converting a Future to a Stream with asStream()

Sometimes, you want to use a Future where a Stream is expected. Dart lets you convert a single Future into a Stream with the method asStream(). This turns the future’s result into a stream event.

Here is a simple example:

Future<String> fetchData() {
  return Future.delayed(Duration(seconds: 2), () => 'Data received!');
}

void main() {

  fetchData().asStream().listen((data) {
    print('Stream got: $data');
  });

}

In this example, the future waits 2 seconds, then sends its result as a single event on the stream. The .listen() method catches this event and prints it.

Conclusion

Using async, await, and Future helps you wait for tasks that take time, like loading data or waiting for a delay. These tools let your Dart code stay clear and easy to read while handling things that happen later. By mastering these, you can write smooth, step-by-step programs without getting stuck waiting. This keeps your apps running well and your code simple to follow.