Last article, we've covered the foundational concepts of the asynchronous programming model, and we learn that synchronous programming in Dart is a powerful way to keep your applications responsive, especially when dealing with tasks like I/O operations, network requests, or heavy computations that could otherwise block the main thread.
Now it's time to dive into how to implement asynchronous code in Dart.
Why does asynchronous code matter?
Asynchronous operations let your program complete work while waiting for another operation to finish. Here are some common asynchronous operations:
- Fetching data over a network.
- Writing to a database.
- Reading data from a file.
Such asynchronous computations usually provide their result as a Future, and to interact with these asynchronous results you can just use callback, or the async and await keywords. These allow you to write code that performs non-blocking operations, ensuring that your application remains responsive and efficient, even when handling time-consuming tasks.
Don't worry, you will understand that now.
Future
A future is an object that represents the result of an asynchronous operation, which will return a value at a later time. They can be thought of as a promise that there will be a value or an error at some point.
The future has two states:
- Completed.
- Uncompleted.
When asynchronous operation is completed, it will return a value if it is successful or it will return an error in the future. But for the uncompleted future, it will eventually return as long as the asynchronous operation is still in progress.
Example of a Future:
String createOrderMessage() {
var order = fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() {
return Future.delayed(
const Duration(seconds: 2),
() => 'Large Latte',
);
}
void main() {
print(createOrderMessage());
}
Output:
Your order is: Instance of 'Future<String>'
As you can see, the asynchronous code works fine but it fails to get the value, here's why:
-
fetchUserOrder()function is an asynchronous, after the 2 sec delay it will provide a string that describes the user's order: a "Large Latte". - The
print()inside themain()will get the user's order by operatingcreateOrderMessage()function, which should callfetchUserOrder()and wait for it to finish. - Because
createOrderMessage()does not wait forfetchUserOrder()to finish,createOrderMessage()fails to get the string value fromfetchUserOrder()function. - So,
createOrderMessage()got a representation of pending work to be done: an uncompleted future.
So... how to get the value from a future?
There are two ways to get value in the future:
- Using a callback.
- Using
asyncandawaitkeywords.
First: Using Callback
When a future completes, you can run a callback to handle the result by using these methods: then(), catchError(), and whenComplete().
You can use then() when the future completes successfully with a value.
void main() {
print('start fetching data');
Future<String>.delayed(Duration(seconds: 2), () {
return 'data are fetched';
}).then((value) {
print(value);
});
print('end fetching data');
}
Output:
start fetching data
end fetching data
data are fetched
But when the future completes with an error value, you can handle it by using thecatchError() method.
void main() {
print('start fetching data');
Future<String>.delayed(Duration(seconds: 2), () {
return 'data are fetched';
}).then((value) {
print(value);
}).catchError((error) {
print(error);
});
print('end fetching data');
}
Output:
start fetching data
end fetching data
data are fetched
Here's another example of using catchError() that will make you understand more:
Future<int> fetchNumber(bool succeed) {
return Future.delayed(Duration(seconds: 2), () {
if (succeed) {
return 42;
} else {
throw Exception("Failed to fetch number");
}
});
}
void main() {
fetchNumber(false).then((value) {
print("The number is $value");
}).catchError((error) {
print("Error occurred: $error");
});
print("Waiting for the number...");
}
Output:
Waiting for the number...
Error occurred: Exception: Failed to fetch number
fetchNumber()function returns aFuture(which is a number) that simulates an asynchronous operation usingFuture.delayed, and after 2 seconds it throws an exception if thesucceedvalue equal tofalseto simulate an error in the operation.
catchError()method will handle any errors that occur during the execution of theFuture, Method After callingfetchNumber().
Also when a future completes with a value or an error, you can just use whenComplete() method. In other words, the callback whenComplete() method will always execute whether the future succeeds or not.
Future<int> fetchNumber(bool succeed) {
return Future.delayed(Duration(seconds: 2), () {
if (succeed){
return 42;
} else {
throw Exception("Failed to fetch number");
}
});
}
void main() {
fetchData(false).then((value) {
print(value);
}).catchError((error) {
print("Error occurred: $error");
}).whenComplete(() {
print("Operation complete, cleaning up...");
});
print("Waiting for the operation to complete...");
}
Output:
Waiting for the operation to complete...
Error occurred: Exception: Failed to fetch number
Operation complete, cleaning up...
Second: Using async and await keywords.
The async and await keywords provide a declarative way to define asynchronous functions and use their results. Remember these two basic guidelines when using async and await:
- To define an
asyncfunction, addasyncbefore the function body. - Use
awaitin front of any expression within anasyncfunction to wait for it to complete. - The
awaitkeyword works only inasyncfunctions.
Here's an example:
import 'dart:async';
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2));
return 'Data fetched successfully!';
}
Future<void> main() async {
print('Fetching data...');
String result = await fetchData();
print(result);
print('Done fetching data.');
}
Output:
Fetching data...
Data fetched successfully!
Done fetching data.
Explanation:
- The
fetchData()function is anasyncfunction which simulates fetching a data from a server and delay the result usingFuture.delayed(Duration(seconds: 2)). - The
awaitkeyword is used beforeFuture.delayed, which causes the function to pause for 2 seconds before returning the result. - The
main()function is alsoasyncbecause it uses theawaitkeyword beforefetchData(), which means themain()function will pause execution untilfetchData()completes. - The
print('Done fetching data.')line will execute only afterfetchData()has returned its result.
Remember:
asyncandawaitare one of the ways to get the value after theFuture.
Error handling in asynchronous functions
Handling errors is crucial in asynchronous programming, you can use try-catch block with async functions to capture any errors that occur during the execution of the await Future.
Example:
Future<void> printOrderMessage() async {
try {
print('Awaiting user order...');
var order = await fetchUserOrder();
print(order);
} catch (err) {
print('Caught error: $err');
}
}
Future<String> fetchUserOrder() {
// Imagine that this function is more complex.
var str = Future.delayed( const Duration(seconds: 4), () {
throw 'Cannot locate user order';
});
return str;
}
void main() async {
await printOrderMessage();
}
Output:
Awaiting user order...
Caught error: Cannot locate user order
Also, you can handle the errors by using callback methods: then(), catchError(), and whenComplete().
Here's an example of that:
Future<void> printOrderMessage() {
print('Awaiting user order...');
return fetchUserOrder().then((order) {
print(order);
}).catchError((err) {
print('Caught error: $err');
});
}
Future<String> fetchUserOrder() {
// Imagine that this function is more complex.
return Future.delayed(const Duration(seconds: 4), () {
throw 'Cannot locate user order';
});
}
void main() {
printOrderMessage();
}
Output:
Awaiting user order...
Caught error: Cannot locate user order
So... what is the difference between both examples?
The main difference between using async/await and using then()/catchError() lies in how the asynchronous code is written and how it affects readability, maintainability, and error handling.
For instance, the async/await syntax makes the code more readable and easier to follow, while then()/catchError() syntax involves chaining method calls, which can become less readable with more complex asynchronous operations.
In error handling, try/catch is straightforward and similar to handling errors in synchronous code, while then()/catchError() error handling can sometimes be less intuitive especially when handling multiple asynchronous calls.
But, that doesn't mean using calling back methods are bad approach, as we said earlier it lies in how the asynchronous code is written and how it affects readability, maintainability, and error handling.
Do want more? here's another example :
Fetching data from the API server
Using callback methods: then(), catchError()
import 'package:http/http.dart' as http;
import 'dart:convert';
void fetchDataUsingCallbacks() {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/1');
http.get(url).then((response) {
if (response.statusCode == 200) {
var data = jsonDecode(response.body);
print('Data fetched successfully: ${data['title']}');
} else {
print('Failed to load data');
}
}).catchError((error) {
print('An error occurred: $error');
}).whenComplete(() {
print('Request complete.');
});
}
void main() {
fetchDataUsingCallbacks();
}
Output:
Data fetched successfully: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Request complete.
Error Case:
An error occurred: <error message>
Request complete.
Using async and await keywords with try(), catch()
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> fetchDataUsingAsyncAwait() async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/1');
try {
final response = await http.get(url);
if (response.statusCode == 200) {
var data = jsonDecode(response.body);
print('Data fetched successfully: ${data['title']}');
} else {
print('Failed to load data');
}
} catch (error) {
print('An error occurred: $error');
} finally {
print('Request complete.');
}
}
void main() async {
await fetchDataUsingAsyncAwait();
}
Output:
Data fetched successfully: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Request complete.
Error Case:
An error occurred: <error message>
Request complete.
Conclusion
Previously in the last couple of articles, we learn what are we have delved into the intricacies of asynchronous programming in Dart, exploring both the async/await syntax and the use of callback methods such as then(), catchError(), and whenComplete(). We learned how asynchronous programming helps keep applications responsive and prevents blocking the main thread, especially during time-consuming operations like network requests or file I/O.
Top comments (0)