Futures in Dart Explained (Part 1) | Darsshan Nair

Fundamental concepts, well-known constructors and useful methods handle asynchronous tasks gracefully

header image by author

In the real world, contracts drawn up between parties by legal bodies involve the transfer (via sale or lease) of an asset or item from one party to another for a specified price over a specified period of time.

These contracts are called futures.

in dart language, Future The class has a working concept similar to a real futures contract that exists in the real world, hence the name. Unlike real-world contracts, it instead involves data transactions.

Futures give us a way to handle asynchronous tasks in a cleaner and more manageable way. Whenever we take advantage of async-await to handle our asynchronous tasks, Dart implements a return value of type Future. This enables us to solve this without breaking the program.

Since this is a really broad topic and it deserves the recognition and clear explanation it deserves, with examples, I’ve decided to make it a two-part article.

In this first part, I will aim to:

  • provide conceptual understanding of Futures, how does it work with event loops to handle asynchronous tasks and states that act as pointers.
  • Discuss the notable constructors and their possible uses.
  • Let us discuss the major methods involved and how they help us to solve the constraints of asynchronous operations.

Dart is a single-threaded programming language. It uses event loop to select one task after another and execute it sequentially. The illusion of background execution of tasks is achieved by Dart even in the absence of background threads.

Future Provides us with the necessary APIs to make it easier to access the event loop and handle asynchronous tasks. This allows us to do the following:

  • Encapsulate the task and send it for processing
  • Identify the current status of the task and its completion
  • get the result if the task is successful
  • Get the error that caused the job to fail
Figure 1: Graphical representation of future operations in the event loop

for us to track the current status of a FutureWe rely on three different states:

  • Incomplete: Work is still in progress.
  • Completed with data: The task is complete and the data is ready.
  • Completed with Error: The task completed, but with an error.

Upon completion, we will use then() to get the result, or we will use catchError() to get the error thrown. It would look something like this:

var list = coffeeList
.then((value) => (value)) // Success
.catchError((error) => (error)); // Failure

These conditions enable us to easily handle UI transitions during an asynchronous process. We can show a loading UI when the status is incomplete, show data when completed with data, or show an error popup when completed with an error.

The point to note here is that the Future API and methods are also close to Promise object in javascript. we can observe the use of then() And how does it unpack the data by resolving the future.

because of this similarity PromiseDevelopers with JavaScript experience will find future usage and functionality easier to digest.

Before we explore our different concepts, let’s establish a foundation for our experiment.

Let’s imagine we’re developing an app that will make it easy to view the latest selection of coffees at our local coffee store across the street.

Here will be our data for hot coffee: https://api.sampleapis.com/coffee/hot

This API gives us an array of hot coffees, and an object will look like this:


"title":"Black",
"description":"Black coffee is as simple as it gets with ground coffee beans steeped in hot water, served warm.",
"ingredients":[
"Coffee"
],
"image":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/A_small_cup_of_coffee.JPG/640px-A_small_cup_of_coffee.JPG",
"id":1

Constructors are the methods which we use to initialize the instance of a class. Some constructors are very useful when creating instances Future Which gives us some major advantages.

Before we move on to more complex implementations of a FutureLet’s take a look at a very basic way of initializing a constructor. Based on the problem stated above we can make a Future which brings us data like this:

void main() 
Future coffeeData =
http.get(Uri.parse('https://api.sampleapis.com/coffee/hot'));

This coffeeData Now we have the data we need, and we can easily solve this using then()and we can see if there is an error by using catchError(), Its as simple as that.

coffeeData.then((value) => (value)) // Success
.catchError((error) => (error)); // Failure

Problem Description

In the early stages of development, we may not have any API to get the data, as it may be developed by the backend team.

We need to develop the UI of our Coffee application since the design is ready, and we have a set of sample data available to work with on the presentation layer.

How do we mock the confusion of HTTP requests and responses in order to develop a UI that accounts well for loading and success states?

Solution

this is where we can take advantage of Future.delayed() the creator. This constructor enables us to create a Future Task that runs within its encapsulation after a period of delay that we specify.

Thus, if we want to develop a loading screen that will be shown during fetch and then show the UI without the actual API being present, we can do it like this:

void main() 
List coffeeList = [
const Coffee(
id: 1,
title: 'Black',
description:
"If you want to sound fancy, you can call black coffee by its proper name: cafe noir.",
ingredients: ['Coffee'],
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/A_small_cup_of_coffee.JPG/640px-A_small_cup_of_coffee.JPG",
)
];

Future.delayed(const Duration(milliseconds: 2000), ()
// The code below will be executed after a delay of 2 seconds
return coffeeList;
);

As you can see from the above code, we have used Future.delayed() To delay the return of mock data by two seconds. This enables us to create the request-response illusion.

Now we can develop loading screens and results screens, and we can even see it in action without any dependency on the API.

Problem Description

We’ve mocked out the scenario in which we might have a loading state before the list of data is returned to us. Thus, we have completed the development of our Coffee app to display a loading UI and a list of coffees. but Future.delayed() always gives us a positive value.

What if we want to simulate a scenario where the request fails for any reason?

Solution

where is it Future.delayed() comes into the picture. we can make one Future Which will return an error instead of a positive value and use it to develop the corresponding failure scenario UI. We can do it like this:

void main()   
final Future apiErrorTest = Future.delayed(const Duration(milliseconds: 2000), ()
// The code below will return an error after a delay of 2 seconds
return Future.error(
Exception('Failed to fetch data from coffee endpoint.'), // Throw
);
);

As you can see from the above code, we are delaying the response by using Future.delayed()but this time it gives a Future.error(), in which there is one exception. Using this, we can simulate a failure of an API fetch call.

In this section we will discuss four main methods which I think are very important for overall methodology Future as an asynchronous event handler.

Then()

This article gives examples of how this works in a few places, but doesn’t deal with it specifically. then() is a callback method that allows us to resolve a given contract Future get more results Future holds if it succeeds.

void main() 
Future coffeeData =
http.get(Uri.parse('https://api.sampleapis.com/coffee/hot'));

coffeeData.then((value) => (value));

In the above example, then() Provides us with the list of coffees we get from the API call.

But what if it is a failure? If we look at this at face value, it seems likely that the call will fail silently, leaving us unable to catch the error. But if we look at the underlying implementation, it has a onError Function Call:

Future then(FutureOr onValue(T value), Function? onError);

it enables then() To catch the error via a global error handler, which catches uncaught errors throughout the app. This fallback is present to ensure that errors do not fail silently.

catch error()

we have seen how then() Returns us a value on success and reports an error without failing silently. even though then() It can do that by relying on a global error handler, it is often recommended that a separate error handler be registered to handle errors gracefully.

where is it catchError() comes into the picture. like then() , catchError() There is also a callback function. This method does not operate individually and is combined with then() To provide a holistic solution. but unlike, then() , catchError() Throws error of asynchronous operation:

void main() 
Future coffeeData =
http.get(Uri.parse('https://api.sampleapis.com/coffee/hot'));

coffeeData.then((value) => (value))
.catchError((error) => (error));

In the above example, if the API call fails, we can handle the error through catchError((error) => (error)) block of code.

catchError() Allows us to add tests to it. Let’s see its implementation below:

Future catchError(Function onError, bool test(Object error)?);

As we can see from the implementation, we can optionally add tests catchError() , This lets us know what kind of error we are facing. Let’s look at an example of testing:

coffeeListFuture.then((value) => (coffeeList = getCoffeeList(value)))
// Custom error catch block
.catchError(
(
Object error,
StackTrace stackTrace,
)
print(error.toString());
,
test: (Object error)
return error is HttpException;

);

In the above block of code, we can see that a catchError() The callback is coupled with a test to check whether it is an HTTP exception type. if the test returns a true Boolean value, then we can deal with this exception based on specific business requirements for HTTP exceptions.

Problem Description

The downside of including test is that it blocks catchError() Will only catch errors that pass this test. Those that do not fit this test will be thrown as uncovered errors. How do we catch those who don’t fit in? What if we needed to identify different types of errors instead of just one?

Solution

This is where the ability to register multiple catchError() the same methods Future Comes to play. we can have more than one catchError() method, each listening for a specific type of error. Let’s see how with the following code:

  coffeeListFuture.then((value) => (coffeeList = getCoffeeList(value)))
// Custom error catch block
.catchError(
(
Object error,
StackTrace stackTrace,
)
print(error.toString());
,
test: (Object error) => error is CustomException)
// Http error catch block
.catchError(
(
Object error,
StackTrace stackTrace,
)
print(error.toString());
,
test: (Object error) => error is HttpException)

In the above example, we can see two catchError() Methods are being combined into one Futurethe very first catchError() One catches custom errors defined by us, and the other enables us to catch HTTPS exceptions.

coffeeListFuture.then((value) => (coffeeList = getCoffeeList(value)))
// Http error catch block
.catchError(
(
Object error,
StackTrace stackTrace,
)
print(error.toString());
,
test: (Object error) => error is HttpException)
// General error catch block
.catchError(
(error) => print(error)
);

In the above example, we created a fail-safe to catch errors that don’t get into HttpException type to avoid errors being treated as unchecked exceptions.

be able to attach multiple catchError(), allows us to define different ways of handling errors for our application. This allows us to handle multiple error scenarios differently, thereby improving the overall user experience of our application.

Applications require continuity regardless of the outcome of a single asynchronous task. We have discussed two scenarios above, a positive scenario when the task is successful, and a negative scenario when the task is a failure, using then() And catchError()respectively.

Problem Description

But what if we want to run a piece of code regardless of the result of that asynchronous task? What if we, say, want to call a method to get a list of cold coffees even though the endpoint fails to provide us with a list of hot coffees?

Solution

where is it whenComplete() comes into the picture. This method allows us to execute code that will run regardless of Future Completes with result or error.

For those of us who come from a JavaScript background, it works like a finally() works for a Promise,

Let us see how this can be implemented. We’ll create a function that returns a Future With HTTP response containing coffee list based on endpoint hot either cold ,

Future fetchCoffeeData(required String coffeeTemp) async 
const baseURL = 'https://api.sampleapis.com/';
final coffeeResponse =
await http.get(Uri.parse(baseURL + '/coffee/' + coffeeTemp));

if (coffeeResponse.statusCode == 200)
return coffeeResponse;
else
throw Exception('Failed to fetch coffees');

Now, let’s make a Future and attach then() , catchError() And whenComplete() To demonstrate what we are talking about.

Future coffeeListFuture = fetchCoffeeData(coffeeTemp: 'hot');

coffeeListFuture
.then((value) => value)
.catchError((error) => error)
.whenComplete(() => fetchCoffeeData(coffeeTemp: 'cold'));

As can be seen, we can execute an instruction that gets a list of cold coffees regardless of the result coffeeListFuture , This gives us a clear idea of ​​what should happen immediately after the initialization Future is completed.

From the above discussions and explanations, we have covered the key concepts around Future How it makes our life easier when dealing with class and asynchronous tasks.

I hope those who read this article now have a good understanding of the following:

  • How Future Works in a single-threaded environment and it is convenient for us to interact with the event loop.
  • how, in short, it’s very similar Promise in javascript.
  • How do we use it to handle asynchronous tasks
  • How can we use API loading Future
  • How to know if asynchronous calls are successful or if they have failed
  • How to mock the loading, success and error states of an API call using the constructor.
  • What are the remarkable methods we have, and why do we need them to handle asynchronous tasks via Futures.

In Futures in Dart Explained (Part 2), I’ll discuss ways to use Futures to solve more complex programming problems.

Leave a Reply