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
async
andawait
keywords.
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 thesucceed
value equal tofalse
to 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
async
function, addasync
before the function body. - Use
await
in front of any expression within anasync
function to wait for it to complete. - The
await
keyword works only inasync
functions.
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 anasync
function which simulates fetching a data from a server and delay the result usingFuture.delayed(Duration(seconds: 2))
. - The
await
keyword is used beforeFuture.delayed
, which causes the function to pause for 2 seconds before returning the result. - The
main()
function is alsoasync
because it uses theawait
keyword 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:
async
andawait
are 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)