paint-brush
Harnessing Concurrent Execution with “Future.wait()” in Dartby@olaoluwa
1,380 reads
1,380 reads

Harnessing Concurrent Execution with “Future.wait()” in Dart

by Olaoluwa AfolabiJune 14th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In Dart, there exists a powerful utility known as `Future.wait()`. This often-overlooked gem enables developers to orchestrate concurrent execution of multiple asynchronous tasks. In this blog post, we will explore the problem domain where 'Future.Wait()` can save the day and delve into best practices.
featured image - Harnessing Concurrent Execution with “Future.wait()” in Dart
Olaoluwa Afolabi HackerNoon profile picture


In the realm of Dart programming, there exists a powerful utility known as Future.wait(). This often-overlooked gem enables developers to orchestrate concurrent execution of multiple asynchronous tasks, leading to improved performance and efficiency.


In this blog post, we will explore the problem domain where Future.wait() can save the day and delve into best practices for utilizing it effectively.

The Problem

Imagine you are developing a Dart application that needs to fetch data from multiple remote APIs simultaneously? That was our case and we got PR for this case scenario in a Dart/Flutter code I am managing, as it is listed in the code snippet below!


Given I can’t share the whole code, I will embody this code smell with the power of Future.wait() later in this blog:


void fetchFromRemoteApis() {
  final apiAFuture = fetchDataFromApi('API A');
  final apiBFuture = fetchDataFromApi('API B');
  final apiCFuture = fetchDataFromApi('API C');

  apiAFuture.then((resultA) {
    print('Received result from API A: $resultA');
    apiBFuture.then((resultB) {
      print('Received result from API B: $resultB');
      apiCFuture.then((resultC) {
        print('Received result from API C: $resultC');
        print('All API calls completed successfully!');
      }).catchError((errorC) {
        print('An error occurred while fetching data from API C: $errorC');
      });
    }).catchError((errorB) {
      print('An error occurred while fetching data from API B: $errorB');
    });
  }).catchError((errorA) {
    print('An error occurred while fetching data from API A: $errorA');
  });
}


In this suboptimal approach, the code chained the then callbacks sequentially for each API call. This results in nested callbacks, commonly known as the "callback hell" or "pyramid of doom" pattern. It leads to less readable and more error-prone code, especially when handling errors.


The code also first fetches data from API A. Upon successful completion, it proceeds to fetch data from API B. Finally, when API B completes successfully, it proceeds to fetch data from API C. At each stage, error handling is implemented using catchError() to capture and handle any errors that may occur during the API calls.


The main disadvantage of this approach is that it lacks the efficiency of concurrent execution, as each API call waits for the previous one to complete before initiating the next. This can lead to longer overall execution time, especially when dealing with multiple independent asynchronous tasks.


Traditionally, when dealing with asynchronous tasks, you might resort to executing them one after another, waiting for each one to complete them before starting the next. This sequential approach can lead to suboptimal performance, especially when network latencies come into play.


Harnessing the Power of Future.wait():  Future.wait(), is a function that enables concurrent execution of asynchronous tasks. With this mighty tool at our disposal, we can dramatically enhance the efficiency of our code by fetching data from multiple remote APIs concurrently, taking advantage of parallelism and reducing overall execution time.


Here's how Future.wait() works:

  1. We create a list of Future objects, each representing an asynchronous task, such as fetching data from a remote API.
  2. We pass this list of Future objects to Future.wait().
  3. Future.wait() returns a new Future that completes when all the provided Future objects have completed.

Saving the Day with Future.wait()

Consider a scenario where your application needs to fetch data from three different remote APIs: A, B, and C. Without Future.wait(), you might fetch data from A, wait for it to complete, then fetch data from B, and finally fetch data from C. This sequential approach can be time-consuming and inefficient.


However, by utilizing Future.wait(), you can kick off all three asynchronous tasks concurrently, reducing the overall execution time. The Future returned by Future.wait() will complete only when all three API calls have finished, allowing you to collect the results efficiently.


So, I reviewed the code smell to make it better. Let’s proceed to that.


Writing Better Code:

To make the most of Future.wait(), here are a few best practices:


  1. Use Future.wait() when the tasks are truly independent and can be executed concurrently. If there are dependencies between tasks, consider using Future.then() or async/await to handle them.

  2. Ensure that the tasks passed to Future.wait() are truly asynchronous and return a Future. If not, consider wrapping synchronous code in a Future using Future.value() or Future.microtask().

  3. Utilize error-handling mechanisms like try-catch or onError callbacks to handle exceptions that might occur during the execution of concurrent tasks.

  4. If there's a need to set a timeout for the completion of Future.wait(), you can use Future.wait(...).timeout(...) to handle situations where certain tasks take an unusually long time.


The Fix:

So here, I embodied the code smell and used Future.wait() to make it better. I have also chosen open random API endpoints for you to see how effective it can serve concurrent execution:


import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;

void main() {
  fetchFromRemoteApis();
}

void fetchFromRemoteApis() async {
  final apiAFuture = fetchDataFromApi('https://jsonplaceholder.typicode.com/posts');
  final apiBFuture = fetchDataFromApi('https://jsonplaceholder.typicode.com/comments');
  final apiCFuture = fetchDataFromApi('https://jsonplaceholder.typicode.com/todos');

  try {
    List<dynamic> results = await Future.wait<dynamic>([apiAFuture, apiBFuture, apiCFuture]);
    
    for (var result in results) {
      print('Received result: $result');
    }
    
    print('All API calls completed successfully!');
  } catch (error) {
    print('An error occurred: $error');
  }
}

Future<dynamic> fetchDataFromApi(String apiUrl) async {
  final response = await http.get(Uri.parse(apiUrl));

  // Check the response status
  if (response.statusCode == 200) {
    final jsonResponse = jsonDecode(response.body);
    return jsonResponse;
  } else {
    throw Exception('API request failed with status ${response.statusCode}');
  }
}


As much as different software engineers can have differing opinions such as using .map() with .then() inside Future.wait(), which I think is unnecessary nested callbacks, or use only .then() with Future.wait() which creates an additional callback function and introduces an extra layer of complexity, I believe the algorithmic complexity of the code smell is reduced drastically by the fix provided in this blog.

Conclusion

In the ever-evolving world of Dart programming, it's essential to leverage the right tools for improved performance and efficiency. Future.wait() is a valuable utility that empowers developers to achieve concurrent execution of asynchronous tasks, effectively reducing execution time and optimizing resource utilization. By incorporating Future.wait() into your code, you can unlock the magic of concurrent programming and embrace the power of Dart to its fullest potential.


So, the next time you find yourself juggling multiple asynchronous tasks, remember the spellbinding capabilities of Future.wait() and watch as your code performs its own brand of magic.


If you need more resources on Future.wait(), kindly refer to https://api.dart.dev/stable/2.1.0/dart-async/Future/wait.html and https://api.flutter.dev/flutter/dart-async/Future/wait.html.


Happy coding!