DEV Community

Ahmed_Greynoon
Ahmed_Greynoon

Posted on

Getting Started with Asynchronous Programming in Dart

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());
}
Enter fullscreen mode Exit fullscreen mode

Output:

Your order is: Instance of 'Future<String>'

Enter fullscreen mode Exit fullscreen mode

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 the main() will get the user's order by operatingcreateOrderMessage() function, which should call fetchUserOrder() and wait for it to finish.
  • Because createOrderMessage() does not wait for fetchUserOrder() to finish, createOrderMessage() fails to get the string value from fetchUserOrder() 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 and await 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');
}
Enter fullscreen mode Exit fullscreen mode

Output:

start fetching data
end fetching data
data are fetched
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

Output:

start fetching data
end fetching data
data are fetched
Enter fullscreen mode Exit fullscreen mode

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...");
}
Enter fullscreen mode Exit fullscreen mode

Output:

Waiting for the number...
Error occurred: Exception: Failed to fetch number
Enter fullscreen mode Exit fullscreen mode
  • fetchNumber() function returns a Future (which is a number) that simulates an asynchronous operation using Future.delayed, and after 2 seconds it throws an exception if the succeed value equal to false to simulate an error in the operation.

  • catchError() method will handle any errors that occur during the execution of the Future, Method After calling fetchNumber().

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...");
}
Enter fullscreen mode Exit fullscreen mode

Output:

Waiting for the operation to complete...
Error occurred: Exception: Failed to fetch number
Operation complete, cleaning up...
Enter fullscreen mode Exit fullscreen mode

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, add async before the function body.
  • Use await in front of any expression within an async function to wait for it to complete.
  • The await keyword works only in async 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.');
}
Enter fullscreen mode Exit fullscreen mode

Output:

Fetching data...
Data fetched successfully!
Done fetching data.
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • ThefetchData() function is an async function which simulates fetching a data from a server and delay the result using Future.delayed(Duration(seconds: 2)).
  • The await keyword is used before Future.delayed, which causes the function to pause for 2 seconds before returning the result.
  • The main() function is also async because it uses the await keyword before fetchData(), which means the main() function will pause execution until fetchData() completes.
  • The print('Done fetching data.') line will execute only after fetchData() has returned its result.

Remember: async and await are one of the ways to get the value after the Future.

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();
}
Enter fullscreen mode Exit fullscreen mode

Output:

Awaiting user order...
Caught error: Cannot locate user order
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Output:

Awaiting user order...
Caught error: Cannot locate user order
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Output:

Data fetched successfully: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Request complete.

Enter fullscreen mode Exit fullscreen mode

Error Case:

An error occurred: <error message> 
Request complete.

Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Output:

Data fetched successfully: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Request complete.

Enter fullscreen mode Exit fullscreen mode

Error Case:

An error occurred: <error message> 
Request complete.

Enter fullscreen mode Exit fullscreen mode

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)