Asynchronous programming in Dart

Asynchronous programming in Dart

Introduction

I wrote this article based on my interest in asynchronous tasks. Asynchronous programming is important for developers because it allows their applications to perform multiple tasks at the same time, without one task blocking the others. For example, when an application needs to wait for data from a server, asynchronous programming allows it to continue running other tasks, rather than waiting for the data before proceeding. This results in a smoother and more responsive user experience for the application's users.

Synchronous And Asynchronous Programming

Explaining to a kid, synchronous programming means doing things one at a time, step by step, and waiting for each step to finish before moving on to the next one. It's like doing your homework one page at a time and not moving to the next page until you finish the one you're working on.

On the other hand, asynchronous programming means doing things at the same time, without waiting for each step to finish before starting the next one. It's like listening to music and drawing a picture at the same time, without having to stop one activity to do the other.

In Dart, we can write programs that work synchronously or asynchronously, depending on what we need to do. Synchronous programming is good for simple tasks that can be done quickly, while asynchronous programming is better for tasks that take a long time or need to run in the background while we do something else.

So, when we write a Dart program, we can choose whether to do things one at a time (synchronously) or things at the same time (asynchronously), depending on what we want to achieve.

What is Future?

Future is used in a dart to denote an operation that is to be completed or not completed. If the task is completed, we get a result otherwise, we return an error. In asynchronous programming, we are forcing the long-processed task to follow an ordered path.

void main() {
  print('Fetching user data...');
  var userData = fetchUserData();
  print('User data received: $userData');
  print('End of main');
}

Future<String> fetchUserData() {
  // Imagine that this function is more complex and slow.
  return Future.delayed(
    Duration(seconds: 3), 
    () => 'John Doe'
  ); // simulate network delay
}
Output:

Fetching user data...
User data received: Instance of '_Future<String>'
End of main

From the code above, The fetchUserData() function returns a Future object that will be completed with the result of the network request after a delay of 3 seconds, represented by the Future.delayed() method. In this case, the result is simply a string containing the name 'John Doe'.

The output shows that the main function is executed synchronously, printing the first message to the console.

Next, the fetchUserData() function is called, which returns a Future<String> object. The returned future is assigned to the userData variable.

The second message is then printed to the console, which shows the value of the userData variable. Since userData contains a Future<String> object, the output displays "Instance of '_Future<String>'" instead of the actual user data.

Finally, the last message is printed to the console, indicating the end of the main function.

The reason for this output is that the fetchUserData() function returns a Future<String> object, which represents the eventual result of the asynchronous operation. When we assign this future object to a variable and try to print it immediately, we see the type of the object instead of its value.

To access the value of the Future object, we need to use the .then() method or the await keyword in an async function.

Using .then() method

The .then() method is used to register a callback function that will be called when a Future object completes.

When we have a Future object representing an asynchronous operation that will complete at some point in the future, we can use the .then() method to attach a callback function that will be executed when the future is resolved with a result.

void main() {
  print('Fetching user data...');
  fetchUserData().then((userData) {
    print('User data received: $userData');
    print('End of main');
  });
}
Output:

Fetching user data...
User data received: John Doe
End of main

In this example, we use the .then() method to register a callback function to be called when the Future returned by fetchUserData() is completed. The callback function takes the userData string as its argument, which is then printed to the console along with another message indicating the end of the main function.

Note that in the example above, the print() statement after the fetchUserData() call has been removed, because we are now using the .then() method to access the result of the Future instead of assigning it to a variable and printing it directly.

What if we place print() after the fetchUserData() call?

void main() {
  print('Fetching user data...');
  fetchUserData().then((userData) {
    print('User data received: $userData');
  });
  print('End of main');
}
Output:

Fetching user data...
End of main
User data received: John Doe

The third message ("End of main") is printed before the Future is completed, because the call to fetchUserData() returns a Future immediately, and the .then() method is used to register a callback function that will be executed later, when the Future is complete.

This example shows how we can use the .then() method to perform some action with the result of an asynchronous operation once it becomes available, without blocking the main thread.

For an ordered execution, you need to place the print inside the .then() callback after userData is retrieved.

Using async/await

The async and await keywords are used to write asynchronous code that looks synchronous and is easier to read and understand, without blocking the main thread.

void main() async {
  print('Fetching user data...');
  var userData = await fetchUserData();
  print('User data received: $userData');
  print('End of main');
}
Output:

Fetching user data...
End of main
User data received: John Doe

The async keyword is used to mark the main function as asynchronous, which allows us to use the await keyword inside the function to pause its execution until a Future is completed with a result.

In this case, when the await keyword is used with the fetchUserData() function call, it pauses the execution of the main function until the Future returned by fetchUserData() is resolved with a result.

Once the Future is completed, the await keyword assigns the result to the userData variable, and the execution of the main function resumes, printing the second message to the console.

Using async and await in this way makes it easier to write and read asynchronous code, because it looks similar to synchronous code, and makes it clear what the flow of the code is. It also makes it easier to handle errors and exceptions that might be thrown by asynchronous operations, because they can be caught using a try-catch block like in synchronous code.

Conclusion

In Dart, asynchronous programming can be achieved using Future objects, which represent a value that may not be available yet, and the async and await keywords, which allow us to write asynchronous code that looks synchronous and is easier to read and understand.