Async programming in Flutter: Futures, async, await, and more

Sohail Mahmud
8 min readDec 20, 2023

--

Asynchronous programming is a way of writing code that can handle several tasks simultaneously without obstructing the primary thread. Flutter uses the Dart language, which supports asynchronous programming by utilizing the Future API, as well as the async and await keywords.

A Future is an object that represents a potential value or error that will be available at some point in the future. Futures can be created, manipulated, and monitored using various methods and operators. For example, you can use the then() method to register a callback function that will be executed when the future completes, or the catchError() method to handle any errors that may occur.

The async and await keywords are syntactic sugar that makes working with futures easier and more readable. The async keyword marks a function as asynchronous, meaning that it returns a future. The await keyword pauses the execution of an async function until a future is resolved, and then returns the value of the future. This way, you can write asynchronous code that looks like synchronous code, without using nested callbacks or complex error handling.

Simple Example

This example simply prints “Doing…” and “Task completed!” to the console. However, the first will appear before the later 2 seconds.

The code:

import 'package:flutter/foundation.dart' show debugPrint;

Future<void> doSomething() {
return Future.delayed(const Duration(seconds: 2), () {
debugPrint('Task completed!');
});
}

void main() {
doSomething();
debugPrint('Doing...');
}

When looking at your terminal window, you’ll see “Doing…“ before “Task completed!” like this:

Fetching Data From APIs

Fetching data from APIs on remote servers is one of the most common use cases of Future, async, and await in Flutter. For convenience, you should install the http package, a Future-based library for making HTTP requests.

To install the http package, add http and its version to the dependencies section in your pubspec.yaml file by executing this:

flutter pub add http

Then run this command:

flutter pub get

The code:

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;

Future<void> loadData() async {
// This is an open api for testing
// Thanks to the Typicode team
final url = Uri.parse('https://api.slingacademy.com/v1/sample-data/users');
try {
final http.Response response = await http.get(url);
final loadedTodos = json.decode(response.body);
if (kDebugMode) {
print(loadedTodos);
}
} catch (err) {
rethrow;
}
}

// Call the _loadData function somewhere
void main() {
loadData();
}

Output:

{"success: true, message: Sample data for testing and learning purposes, total_users: 1000, offset: 0, limit: 10, users: [{id: 1, gender: female, date_of_birth: 2002-04-26T00:00:00, job: Herbalist, city: Humphreyfurt, zipcode: 79574, latitude: 13.032103, profile_picture: https://api.slingacademy.com/public/sample-users/1.png, email: kayla.lopez.1@slingacademy.com, first_name: Kayla, last_name: Lopez, phone: +1-697-415-3345x5215, street: 3388 Roger Wells Apt. 010, state: Vermont, country: Jordan, longitude: 112.16014}, {id: 2, gender: female, date_of_birth: 1924-05-14T00:00:00, job: Technical author, city: West Angelaside, zipcode: 44459, latitude: 51.5214995, profile_picture: https://api.slingacademy.com/public/sample-users/2.png, email: tina.patrick.2@slingacademy.com, first_name: Tina, last_name: Patrick, phone: 800-865-4932, street: 4564 Gamble Light Suite 885, state: Kansas, country: Greenland, longitude: -21.22766}, {id: 3, gender: female, date_of_birth: 1998-04-23T00:00:00, job: P<…

Futures allow us to work with asynchronous code, let’s pause for 1 minute here….

When you write code you could have Synchronous code and Asynchronous code.

With Sync code we mean the normal code that you write, it works in a linear way, an instruction then follows the next one, then the next, you know that the last line it is the last one that will be executed!

On the other hand, Async code works in a non linear way. it waits for some code to return in the future, a function that is Async, it will run when it has done its job.

I KNOW, I KNOW, IT IS NOT TOTALLY CLEAR… LET ME ELABORATE BETTER

You define a code asynchronous by using the async keyword before the body; Now that your code will be async you can use inside the body the await keyword (await can be used only if you define a code asyncronous).

For example, now I’ll use same invented name function that late InshaAllah I’ll use in the code to let you understand how everything works.

You should use the await keyword for a Future function, in our example we are going to call it “getSomethingFromInternet”, in this way you are saying that function has something that will return in the future (We don’t know when and how much it takes), after the Future end his job, will return a Future of the type defined by the future, in our case “String” since we have defined our Future as “Future”;

Future” means that we expect a string when the future is completed.

Note: On async code, all the code before the await will run synchronously, when it finds the first await it will wait for the value to completely return, as completed, with value or error!
Now, on our main we use a variable to store our future, calling await under our Future like this:

String gotSomething = await getSomethingFromInternet();

We want to store the result of our function inside the “gotSomething” variable, If we don’t use the await keyword we will have inside the variable a Future, but since we are using it, at the end we will have a string with the value “Something from the internet”.

// We define our function as async
void main() async {
print('This line runs before await');

// Until here the code get executed syncronously
// Here it see the await keyword, it waits to to value to return
// In our case will return a string with
// "Something from the internet" after 4 seconds
String gotSomething = await getSomethingFromInternet();

// After 4 seconds continues to execute the remaining code
print('This line waits for the values from the future');

// Now we are ablet to use our variable with value fetched asynchronously
print(gotSomething);

}

// We define a function that will return something from the future
Future<String> getSomethingFromInternet(){
return Future.delayed(
const Duration(seconds: 4),
() => 'Something from internet',
);
}

In this example when we use Futures in this way waiting for 4 seconds we simply are simulating an internet call without going too much specific, in a real case scenario you will use usually futures to make calls to a web API for example.

APIs have endpoints that could return something, as explained in my previous article; When you define for example an endpoint from your Rest API and you want to get the results from Flutter you use Futures cause you don’t really know if and when the server would reply;

The server could reply to you after 10 seconds cause you are in U.S. and my server is in Germany, or to me could reply after 1 second.

We don’t know how it will reply, if the server at the moment of the call is working fine and it has no problems, or if it is down because some black hat hacker just attacked it.

To make things simple and understandable we use this simple example.

But, since we don’t know

But, there is a but, there is always a but!

Since we don’t really know if the server will reply with a value or with an error, that means that we should take some sort of precautions, am I right?

We cannot pretend that it will always return us a value because it doesn’t work in this way, if you call a web API endpoint you don’t know if at the moment it is working or it has some issues, if you made the wrong query etc.

The way to be safe and not break your application is by using the try-catch keywords.

With those keywords you are basically saying “Please try this code, if there is any error then catch it and execute the catch block”; When there is any error, the execution inside the try block stop and the catch block executed the code inside it with the error received as parameter.

Now, we should really do an example to make things more interesting and easy to understand!

This is an example that I found in the dart documentation.

// It is void cause we are only printin the order value
Future<void> printOrderMessage() async {
try {

// We use the await keyword get the async code
var order = await fetchUserOrder();
print('Awaiting user order...');
print(order); // We print our content

} catch (err) {
print('Caught error: $err');
}
}

Our application starts from here, as explained before the code inside the try runs but if we have any error the code stops from there and continues inside the catch block;

Now we really want to know what’s inside fetchUserOrder so we are able to understand better the flow of the code.

// Here we return a string if the future completes with value
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;
}

Here if you watch carefully you’ll see a throw, which means that the function will throw an error “Cannot locate user order”.

Well let’s take a look again at our printOrderMessage

// THIS IS A COPY OF THE PREV FUNCTION
Future<void> printOrderMessage() async {
try {
// We use the await keyword get the async code
var order = await fetchUserOrder();
print('Awaiting user order...');
print(order); // We print our content
} catch (err) {
print('Caught error: $err');
}
}

Now that we know that our cool function fetchUserOrder() will throw an error, we know that when it reaches the point where it is called the flow of our code stops from there and moves to the catch block of code;

Inside the error parameter, we will be able to access our error!

But why we should use try and catch?

That’s a good question! It seems that we are complicating our life by adding try and catch, but in reality we are not.

When we write code and there is any error that means that the execution of the code is going out of our plan, the plan that we have written, you user must follow the main road and not go outside!

If we have errors it also means that our application will break and eventually stop.

We use them so we can have a plan B ready, in case if…

There are some cases where you want to stop the user from going forward, as an example you have the classic user login.

In other cases you want your user to still continue his navigation, for example, if the user wants to add an article as bookmark but you have some issues in that specific endpoint, you may want to show a simple popup that says something like “We have some troubles now, please retry later” or “We are updating our service, please retry later”, you probably come over one of this cases.

Another useful thing that you may want to do is to save the errors in a log file to be accessed in the future by you (Developer) to have a better view on why things brake down so you could improve you application.

Conclusion

That’s the end my friend, inshAllah (God willing) I hope that you found this content easy, enjoyed it and most importantly, you have learned something from it!

Please let me know if and how I could improve more my content.

If you still have some troubles, please check out the Dart and Flutter official docs related to this.

--

--

Sohail Mahmud
Sohail Mahmud

No responses yet