Dart: Futures and Streams

Learn how to use Futures and Streams for writing asynchronous code in dart By Michael Katz.

5 (1) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Future Types

So far, you’ve seen delayed, which performs an operation after a time delay. Two other handy Future constructors are value and error.

To try them, replace main once again with:

void main() {
  final valueFuture = Future.value("Atlanta");
  final errorFuture = Future.error("No city found");

  valueFuture.then((value) => print("Value found: " + value));
  errorFuture.then((value) => print("Value found: " + value));
}

As you can see, Future.value needs a value and Future.error needs an error description.

When run, you get the following output:

Value found: Atlanta
Uncaught Error: No city found

Notice that the error future’s then is not evaluated and an uncaught error is recorded in the console. You’ll learn about error handling in the next section.

A few other constructor functions are available for creating Future instances. Of those, sync might be the handiest.

Add the following helper function:

int calculatePopulation() {
  return 1000;
}

This simulates a population calculation. It’s a regular function that returns immediately.

Next, in main, add to the bottom:

valueFuture
  .then((value) => Future.sync(calculatePopulation))
  .then((value) => print("Population found: $value"));

The sync constructor is similar to the value constructor. The main difference is it’s also possible for sync to error as well as complete. It’s most useful in an operation like this where you want to run some synchronous code in response to an asynchronous one.

You’ll mostly use a future in two main ways. One way is by wrapping an async function; the other is by transforming futures returned from APIs and packages. You’ll learn more about these in a future section.

Dealing with Errors

As you’ve seen, some futures can produce errors. That can include network i/o errors, disconnects or data deserialization errors.

Error handling is built into the Future API. The way to handle errors is to catch them and then act. For example, add the following to main:

errorFuture
  .then((value) => print("Then callback called."))
  .catchError((error) => print("Error found: " + error));

A catchError callback is provided, which catches the error. If you run and look in the console, you’ll see the then callback was not run, but the catchError callback was.

Error found: No city found

Futures can have a chain of then callbacks that transform the results of one operation with one catch block at the end of the chain. This single block will handle any error along the way. For example:

errorFuture
  .then((value) => print("Load configuration"))
  .then((value) => print("Login with username and password."))
  .then((value) => print("Deserialize the user information."))
  .catchError((error) => print("Couldn't load user info, please try again"));

In this example, you can imagine a multistep startup sequence where no matter where it fails, you can prompt the user to try again.

You can also have different error handling for different errors. For example, replace main again with:

class NetworkError implements Exception {}
class LoginError implements Exception{}

void main() {
  final errorFuture = Future.error(LoginError());

  errorFuture.then((value) => print("Success!"))
    .catchError((error) => print("Network failed, try again."),
                test: (error) => error is NetworkError)
    .catchError((error) => print("Invalid username or password."),
                test: (error) => error is LoginError)
    .catchError((error) => print("Generic error, log it!"));
}

This creates two custom exception types: NetworkError and LoginError. Then, with a series of catchError calls, it uses the optional test parameter to run various callbacks depending on the type of error. You could use the test parameter to check HTTP codes or error descriptions.

Using Async and Await

If you like writing code that performs asynchronous operations, but you don’t like the chaining of callbacks, the async/await keyword pair will be useful.

Denoting a function as async lets the compiler know that it won’t return right away. You then call such a function with an await keyword so the program knows to wait before continuing. You saw it earlier with the simulated network operation delay, but now you’ll learn to use it.

First, recall that when using a Future, the return value has to be in a then callback. For reference, replace main with:

void main() {
  print("Loading future cities...");
  // 1
  fetchSlowCityList().then(printCities);
  // 2
  print("done waiting for future cities");
}

In this example, you’ll notice:

  1. The data handling function is set with the future’s then method.
  2. This print is executed immediately after starting the future, so the print happens in the console before the city list prints.

This can be rewritten to be easier to read and to get the print statement to print after the work is finished. Once again, replace main with:

// 1
void main() async {
  print("Loading future cities...");
  // 2
  final cities = await fetchSlowCityList();
  // 3
  printCities(cities);
  // 4
  print("done waiting for future cities");
}

This modification uses await when calling the Future, making the code more linear. Here’s some details:

  1. First, the main function has the async keyword. This indicates the function will not immediately return. It’s required for any function that uses an await.
  2. By using the await keyword here, you can assign the value of the completed Future to a local variable instead of having to wrap the value handler in a completion block. Program execution will stop on this line until the Future completes.
  3. By waiting for completion, the code can now ensure that cities will have a value so you can use it as an input to another function such as printCities.
  4. The final print statement will happen after the city list is printed, which you can verify by running the program and checking the console.

Take a look at an async function. You added this function earlier:

Future<List<String>> fetchSlowCityList() async {
  print("[SIMULATED NETWORK I/O]");
  await Future.delayed(Duration(seconds: 2));
  return fetchCityList();
}

The consequence of using an await inside a function is you must annotate the function definition with the async keyword. That means the function does not immediately return. Also, you have to designate the function’s return type is a Future. In this case, the return value is the output of fetchCityList(), which is a List. By using the async keyword, this value is wrapped in a Future, and thus the function definition needs to indicate that.

The return value is a List. When it’s used in main, the assignment to cities with the await treats this value’s type as List. When using async/await, other than in function definitions, you needn’t worry about the Future type.