Asynchronous programming in Dart

Let’s dive into asynchronous programming in Dart! I’ll break it down into simple topics and sub-topics, explain everything in plain words, and use examples to make it clear. Asynchronous programming is all about handling tasks that take time (like fetching data from the internet) without freezing your app. Dart makes this easy with some cool tools. Ready? Let’s go!


1. What is Asynchronous Programming?

Asynchronous programming lets your code do multiple things at once. Imagine you’re cooking dinner: you put water on to boil and while waiting, you chop veggies. You don’t just stand there staring at the pot! In Dart, asynchronous code works the same way—it runs tasks in the background while the rest of your program keeps going.

Key Idea:

  • Synchronous: Code runs step-by-step, waiting for each task to finish.

  • Asynchronous: Code starts a task (like fetching data) and moves on without waiting.


2. Why Use Asynchronous Programming?

  • To avoid freezing your app when doing slow tasks (e.g., loading a file, calling an API).

  • To make your app feel fast and responsive.


3. Dart’s Asynchronous Tools

Dart gives us three main tools to work with asynchronous code:

  1. Future: Represents a value that will be available later.

  2. async and await: Makes asynchronous code look like regular (synchronous) code.

  3. Stream: Handles a sequence of values over time (like a live video feed).

Let’s explore each one!


4. Topic 1: Futures

A Future is like a promise: "I’ll give you the result when I’m done." It’s used for tasks that take time, like downloading a file.

Sub-Topics:

a. Creating a Future

dart

Future<String> fetchMessage() {
  return Future.delayed(Duration(seconds: 2), () => "Hello, Dart!");
}
  • Future.delayed waits 2 seconds, then returns "Hello, Dart!".

  • This mimics a slow task, like fetching data.

b. Using a Future with .then()

dart

void main() {
  print("Starting...");
  fetchMessage().then((message) => print(message));
  print("Done!");
}

Output:

Starting...
Done!
Hello, Dart! (after 2 seconds)
  • .then() runs when the Future finishes.

  • Notice "Done!" prints first—proof the code didn’t wait!

c. Handling Errors

Futures can fail (e.g., network error). Use .catchError():

dart

Future<String> fetchMessage() {
  return Future.delayed(Duration(seconds: 2), () {
    throw "Oops, something went wrong!";
  });
}

void main() {
  fetchMessage()
      .then((message) => print(message))
      .catchError((error) => print("Error: $error"));
}

Output:

Error: Oops, something went wrong! (after 2 seconds)

5. Topic 2: async and await

Writing .then() chains can get messy. Dart’s async and await make it simpler by letting you write asynchronous code like it’s synchronous.

Sub-Topics:

a. Using async and await

  • Mark a function with async to use await.

  • await pauses the function until the Future finishes.

dart

Future<void> printMessage() async {
  String message = await fetchMessage();
  print(message);
}

void main() async {
  print("Starting...");
  await printMessage();
  print("Done!");
}

Future<String> fetchMessage() {
  return Future.delayed(Duration(seconds: 2), () => "Hello, Dart!");
}

Output:

Starting...
Hello, Dart! (after 2 seconds)
Done!
  • await waits for fetchMessage() to finish before moving on.

  • main() is marked async because it uses await.

b. Error Handling with try-catch

dart

Future<void> printMessage() async {
  try {
    String message = await fetchMessage();
    print(message);
  } catch (error) {
    print("Error: $error");
  }
}

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

Future<String> fetchMessage() {
  return Future.delayed(Duration(seconds: 2), () {
    throw "Failed to fetch!";
  });
}

Output:

Error: Failed to fetch! (after 2 seconds)

6. Topic 3: Streams

A Stream is like a Future, but instead of one value, it gives you many values over time—like a YouTube video streaming data as you watch.

Sub-Topics:

a. Creating a Stream

dart

Stream<int> countDown() async* {
  for (int i = 3; i > 0; i--) {
    await Future.delayed(Duration(seconds: 1));
    yield i; // Sends a value to the stream
  }
}
  • async* marks a function that returns a Stream.

  • yield sends each value one by one.

b. Listening to a Stream

dart

void main() {
  print("Starting countdown...");
  countDown().listen((number) {
    print(number);
  });
  print("This runs immediately!");
}

Output:

Starting countdown...
This runs immediately!
3 (after 1 second)
2 (after 2 seconds)
1 (after 3 seconds)
  • .listen() runs a function for each value in the stream.

c. Handling Stream Errors

dart

Stream<int> countDownWithError() async* {
  yield 3;
  await Future.delayed(Duration(seconds: 1));
  yield 2;
  await Future.delayed(Duration(seconds: 1));
  throw "Countdown crashed!";
}

void main() {
  countDownWithError().listen(
    (number) => print(number),
    onError: (error) => print("Error: $error"),
  );
}

Output:

3
2 (after 1 second)
Error: Countdown crashed! (after 2 seconds)

d. Using await for with Streams

dart

Future<void> printCountdown() async {
  await for (int number in countDown()) {
    print(number);
  }
  print("Countdown finished!");
}

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

Output:

3 (after 1 second)
2 (after 2 seconds)
1 (after 3 seconds)
Countdown finished!
  • await for waits for each value in the stream.

7. Topic 4: Combining Futures and Streams

Sometimes you’ll mix Futures and Streams. For example, fetching initial data (Future) and then listening to updates (Stream).

Example:

dart

Future<String> fetchUser() {
  return Future.delayed(Duration(seconds: 1), () => "User: Alex");
}

Stream<String> userUpdates() async* {
  yield "Update 1";
  await Future.delayed(Duration(seconds: 1));
  yield "Update 2";
}

void main() async {
  String user = await fetchUser();
  print(user);
  await for (String update in userUpdates()) {
    print(update);
  }
}

Output:

User: Alex (after 1 second)
Update 1
Update 2 (after 1 more second)

8. Topic 5: Practical Tips

a. Don’t Block the Main Thread

  • Always use async/await or Futures for slow tasks.

  • Example: File reading, network calls.

b. Use Future.wait() for Multiple Futures

Run several Futures at once and wait for all:

dart

void main() async {
  List<Future<String>> tasks = [
    Future.delayed(Duration(seconds: 2), () => "Task 1"),
    Future.delayed(Duration(seconds: 1), () => "Task 2"),
  ];
  List<String> results = await Future.wait(tasks);
  print(results); // [Task 1, Task 2] (after 2 seconds)
}

c. Cancel Streams

Stop listening to a Stream when you don’t need it:

dart

void main() {
  Stream<int> numbers = countDown();
  var subscription = numbers.listen((number) => print(number));
  Future.delayed(Duration(seconds: 2), () => subscription.cancel());
}
  • Cancels after 2 seconds, so only "3" and "2" print.

9. Final Example: A Mini App

Let’s tie it all together:

dart

Future<String> fetchWeather() {
  return Future.delayed(Duration(seconds: 2), () => "Sunny");
}

Stream<String> weatherUpdates() async* {
  yield "Cloudy";
  await Future.delayed(Duration(seconds: 1));
  yield "Rainy";
}

void main() async {
  print("Fetching weather...");
  String weather = await fetchWeather();
  print("Today: $weather");

  print("Listening for updates...");
  await for (String update in weatherUpdates()) {
    print("Update: $update");
  }
}

Output:

Fetching weather...
Today: Sunny (after 2 seconds)
Listening for updates...
Update: Cloudy
Update: Rainy (after 1 more second)

Summary

  • Futures: Handle one delayed value with .then() or await.

  • async/await: Write clean, readable async code.

  • Streams: Handle multiple values over time with .listen() or await for.

  • Use them together for real-world apps!

Practice these examples in DartPad (dartpad.dev) or your editor. Got questions? Ask me anything!

0
Subscribe to my newsletter

Read articles from Singaraju Saiteja directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Singaraju Saiteja
Singaraju Saiteja

I am an aspiring mobile developer, with current skill being in flutter.