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.