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:
Future: Represents a value that will be available later.
async and await: Makes asynchronous code look like regular (synchronous) code.
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!
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.