15. Asynchronous Tasks

Overview

By the end of this chapter, you will be able to implement asynchronous programming and its different techniques; explore the pitfalls of callback hell and the pyramid of doom; illustrate the use of promises to execute code upon operation completion; use the new async/await syntax to make asynchronous code look and feel almost sequential; and apply the Fetch API to make remote service calls.

Introduction

Asynchronous tasks allow the execution of the main thread of a program to proceed even while waiting for data, an event, or the result of another process, and achieve snappier UIs as well as allowing some types of multitasking.

Unlike other languages that can have many concurrent threads executing, JavaScript typically runs in a single thread. So far, you have already learned in detail about how JavaScript's single-threaded model is enabled by the event loop and the associated event queue. Under the hood, the browser or the Node.js runtime has background threads that listen for events or issue service calls, and when a new event is captured or a service call responds, it is pushed into the event queue. JavaScript continually scans the event queue and triggers the handlers for those events when available. Event handlers are most commonly callback methods, but there are other types as well, such as Promises, which you will learn about in this chapter.

Some threads take longer than others. In a restaurant, preparing a steak takes more time than fulfilling an order for a glass of wine. However, since there is no dependency between these items, they can each be performed concurrently. But even if the wine was ordered minutes after the steak, there is a good chance the wine will be brought over before the steak, even by the same worker. This is essentially the idea of asynchronous processing. (To take the analogy a bit further, when each item is ready to be served to the customer, they will be placed in the worker's queue by the kitchen staff. The worker constantly checks their queue for more things to bring to the restaurant's patrons.)

The early versions of JavaScript mostly used callbacks to achieve asynchrony, but the negative consequences of creating callback hell soon became apparent, as you will see. Then, in ECMAScript 2015, an alternative was introduced, called Promises, which helped a lot but still left a bit to be desired. More recently, new keywords and syntax known as async/await were added in ECMAScript 2017, which simplified asynchronous code even further and made it resemble regular sequential code in many respects. You will explore each of these in the sections that follow.

In this chapter, you will also revisit TheSportsAPI that was introduced in Chapter 10, Accessing External Resources, which you used to query and retrieve sports-related data regarding teams, game scores, players, and upcoming events. It may be a good idea to reread that chapter to refresh your memory, as we will be expanding upon the material there.

Callbacks

As you explored in Chapter 10, Accessing External Resources, callbacks are the oldest and simplest means of executing asynchronous functionality in JavaScript. A callback is a specified function to be called once the result of an operation is ready. You saw this with the jQuery $.ajax() and $.getJSON() methods, where a function is called once a successful service call response is available; for example:

$.getJSON('https:/www.somesite.com/someservice',

      function(data) {

      // this function is a callback and is called once

             // the response to the service call is received

      }

    );

Another area where callbacks are heavily used is for event handlers. Events can be considered asynchronous, as they can happen at unpredictable times and in any order. The callbacks to handle events are typically registered with the browser runtime and added to the event queue when calling addEventListener().

setTimeout()

The setTimeout() function is the traditional way of scheduling code to run asynchronously at some point in the future. It is most commonly called with a parameter that specifies the number of milliseconds to wait before execution.

Exercise 15.01: Asynchronous Execution with setTimeout()

This exercise demonstrates how execution flows when using setTimeout() if that parameter is specified as 0 or just omitted:

  1. In the Google Chrome browser, go into Developer Tools (the menu with three dots at the upper-right corner of the screen) | More Tools | Developer Tools, or just hit the F12 key).
  2. In the Console tab, paste the code from the following file, but do not hit Enter yet. You can find the code in the file exercise1.js.

    console.log("start");

    setTimeout(function() {

            console.log("in setTimeout");

        }, 0);

    console.log("at end of code");

    Consider the code you pasted. You might think the function in the setTimeout() block would execute right away since it was specified to execute after zero milliseconds. But, in fact, this is not what happens. So, let's see the output.

  3. Press the Enter key in the console to execute the code. The output will be the following:

    start

    at end of code

    in setTimeout

    Due to the way asynchronous processing works, the callback in setTimeout() is placed in the event queue to schedule it for later processing, while the execution of the main code proceeds. The callback will not get executed until the main code completes.

    The overuse of setTimeout() can also lead to bad coding practices, as we will see in the next section.

Callback Hell and the Pyramid of Doom

Callbacks are perhaps the simplest and most straightforward approach to handling asynchronous requests, but if you are not careful, your code can get messy and unmanageable very quickly. This is especially true if you need to make a series of nested asynchronous service calls that depend on data returned from the previous call.

Recall TheSportsDB from Chapter 10, Accessing External Resources. Let's say you have a requirement to obtain a list of honors granted to the players of your favorite team.

In most cases, you would not know the identifiers for the player id parameter required by the API in advance. Consequently, you would need to first use an API service call to look at the team ID up, in order to obtain the player list. But there's a further caveat, it turns out that in order to do that, you now need to also know the identifier for the league of which the team is a part. Since you don't know the league ID, you need to find the league ID itself using yet another service.

For such requirements, you may end up with code that looks like the following code snippet (don't worry if you don't understand the code yet, as it will be covered in depth later). You will find the code of file pyramid_of_doom_example.html on GitHub in the following location:

// Pyramid of DOOM!!!

$.getJSON(ALL_LEAGUES_URL, function(leagueData) {

    const leagueId = findLeagueId(leagueData, LEAGUE_NAME);

    $.getJSON(ALL_TEAMS_URL, {id: leagueId}, function(teamData) {

        const teamId = findTeamId(teamData, TEAM_NAME);

        $.getJSON(ALL_PLAYERS_URL, {id: teamId}, function(playerData) {

            playerData.player.forEach(player => {

                $.getJSON(PLAYER_HONORS_URL, {id: player.idPlayer},

                    function(honorData) {

                        printHonors(honorData);

                    }

                );

            });

        });

    });

});

In other words, here is a case where, in order to get one piece of data in one call, there are dependencies on the results of other calls. Each callback uses the result of the previous call to invoke further calls.

Notice all the nested blocks that resulted from using callbacks. It starts with one function, which then includes another function, and then multiple levels of more functions within functions, and this results in a series of unruly end-bracket and end-parenthesis characters. The shape of this code resembles a pyramid rotated on its side, and therefore has the slang term of the pyramid of doom:

In this section, you revisited how asynchronous logic is traditionally implemented in JavaScript, and how using callbacks can get you into trouble and result in hard-to-manage spaghetti code. You also familiarized yourself with TheSportsDB API and implemented some new functionality that makes requests of it.

There are several alternatives to using callbacks for asynchronous processing that have been developed in recent years, including promises and the new async/await syntax. The next section will explore promises, which are a major improvement over callbacks, as you will see.

Promises and the Fetch API

In a nutshell, a promise is an object that wraps asynchronous logic and provides methods to access the results or errors once operation completes. It is a proxy for the result value until it is known, and allows you to associate handler functions rather than using callbacks. It is a promise to supply the value once it is known and available.

To get a good feel for how promises are used, you will first be introduced to the Fetch API, which uses promises heavily. Then, we will backtrack and dive into a detailed description of the promises themselves.

Fetch is another API that enables you to make network requests and REST service calls, similar to jQuery's AJAX methods or the native XMLHttpRequest. The main difference is that the Fetch API uses promises, which has a cleaner and more concise syntax that helps you avoid callback hell.

Typical Fetch API usage for a JSON request looks something like this:

fetch(someURL)

      .then(response =>response.json())

      .then(jsonData =>parseSomeDataFromResponse(jsonData))

      .then(someData =>doSomethingWithDataObtained(someData))

      .catch(error => console.log(error));

The fetch() call invokes the service call in the URL. Once a valid response is available, the function in the first then() block is executed. It receives the response as an argument, and, in this case, runs the json() method on it to convert the text into an object. The result of this method call is then made available to subsequent then() methods down the chain. Errors can also be handled by catch() methods.

Using the Fetch API to Get Player Honors

In this section, we will discard the jQuery callback methods used earlier to obtain player honor data and instead take an approach that utilizes promises (this gets us out of the callback Pyramid of Doom).

The Fetch API is relatively low-level and does not offer as many freebies as jQuery's $.ajax() and $.getJSON() functions, so we'll create a wrapper function called myFetch() to make the usage a bit nicer for our use case; specifically:

  • Fetch only takes a full URL and does not encode parameters for you. The myFetch() function will include an optional second parameter for params as key-value pairs, which, if specified, will encode the parameter values and append the resulting query string to the URL.
  • Fetch does not automatically parse the JSON response, so you'll include this in myFetch().
  • Fetch does not consider an HTTP status code as an error condition unless the code is 500 or above. But for our purposes, any response other than 200 (OK) should be considered an error. You'll add a check for this.

    Note

    This wrapper is not appropriate for all use cases. You should tailor it to your particular needs.

Exercise 15.02: Refactoring the Honors List to Use the Fetch API

In this exercise, we will refactor the code to obtain a list of honors granted to the players of your favorite team. We will refactor it to use the Fetch API:

  1. First, we will create a file that contains common pieces of code that will be used throughout this chapter. In a text editor or IDE, enter the following initial chunk of code. You can also find the code of file players.js on GitHub in the file location: https://packt.live/2KUdBY4

    // hard coded data for purposes of illustration

    const LEAGUE_NAME = "English Premier League";

    const TEAM_NAME = "Arsenal";

    const BASE_URL = "https://www.thesportsdb.com/api/v1/json/1/";

    const ALL_LEAGUES_URL = BASE_URL + "all_leagues.php";

    const ALL_TEAMS_URL = BASE_URL + "lookup_all_teams.php";

    const ALL_PLAYERS_URL = BASE_URL + "lookup_all_players.php";

    const PLAYER_HONORS_URL = BASE_URL + "lookuphonors.php";

    This code has the URLs and data values for remote services we will be calling of TheSportsDB API.

  2. Enter the following myFetch() method:

    Function myFetch(url, params) {

        if (params) {

            url += "?" + encodeParams(params);

        }

        return fetch(url)

            .then(response => {

                if (!response.ok) {

                    throw new Error(response.status);

                }

                Return response.json()

            }

        );

    }

    This is the implementation of the wrapper function to fetch() that was mentioned earlier.  First, if one or more or more parameter key-value pairs are specified, they are encoded into a query string and appended to the URL.  After this, the fetch() function is called, and then() is executed when the response is available. If the HTTP status code is anything other than 200 (OK), an error is thrown. This causes it to be caught by the catch() function (if defined in the promise call chain). Finally, if all is okay, it calls response.json() to parse the JSON response into an object, which is returned as another promise to be passed along and resolved in the subsequent then() function.

  3. Use the following helper function, which encodes the key-value pair parameters to be appended to the query string of the URL:

    Function encodeParams(params) {

        return Object.keys(params)

            .map(k => encodeURIComponent(k) + '=' +

                      encodeURIComponent(params[k]))

            .join('&');

    }

  4. Now, write the findLeagueId() function:

    Function findLeagueId(leagueData, leagueName) {

        const league = leagueData.leagues.find(l => l.strLeague === leagueName);

        return league ? league.idLeague : null;

    }

    This code takes the result of the ALL_LEAGUES_URL service call and utilizes find() to locate the result that matches the desired league name. Once found, it returns the ID for that league (or null if there was no match found).

  5. Write the findTeamId() function as follows:

    Function findTeamId(teamData, teamName) {

        const team = teamData.teams.find(t => t.strTeam === teamName);

        return team ? team.idTeam : null;

    Similar to the last function, this code takes the result of the ALL_TEAMS_URL service call and uses find() to locate the desired team.

  6. Enter the printHonors() function:

    Function printHonors(honorData) {

        if (honorData.honors != null) {

            var playerLI = document.createElement("li");

            document.getElementById("honorsList").append(playerLI);

            var playerName =

                document.createTextNode(honorData.honors[0].strPlayer);

            playerLI.appendChild(playerName);

            var honorsUL= document.createElement("ul");

            playerLI.appendChild(honorsUL);

            honorData.honors.forEach(honor => {

                var honorLI = document.createElement("li");

                honorsUL.appendChild(honorLI);

                var honorText = document.createTextNode(

                    `${honor.strHonour} - ${honor.strSeason}`);

                honorLI.appendChild(honorText);

            });

        }

    }

    This function takes the result of the PLAYER_HONORS_URL service call and creates a list of player honors comprising the <ul> and <li> HTML tags.

  7. We have now completed the common functions. Save this file with the filename players.js.
  8. Create a new file in your editor or IDE. Enter the initial chunk of code from the following file. You can find the code on GitHub in the file location: https://packt.live/2XRGLMO

    <html>

    <head>

        <meta charset="utf-8"/>

        <script src="players.js"></script>

    </head>

    <body>

    Arsenal Player Honors:

    <ul id="honorsList"></ul>

    <script>

  9. Enter the following, which starts to replace the jQuery $.getJSON code with calls to the Fetch API:

    myFetch(ALL_LEAGUES_URL)

      .then(leagueData => {

          const leagueId = findLeagueId(leagueData, LEAGUE_NAME);

          return myFetch(ALL_TEAMS_URL, {id: leagueId});

      })

    Processing begins with calling the myFetch() wrapper function to invoke the service call that retrieves a list of all leagues. Once the response is available, the function specified in the then() method is invoked.

    Note

    There is no need to check for HTTP errors and you can assume the response was valid since error checking was already done in the implementation of the myFetch() function call outlined above. You also do not need to parse the JSON to an object.

    The findLeagueId() function is then called to find the ID of the league you are interested in, which is needed for the next service call to get the teams in the league. Once found, myFetch() is then called again. The promise returned by the myFetch() function call is then returned, to be passed along for processing by the following then() block.

  10. Enter the next then() clause to obtain the team ID:

      .then(teamData => {

          const teamId = findTeamId(teamData, TEAM_NAME);

          return myFetch(ALL_PLAYERS_URL, {id: teamId});

      })

    In a similar fashion, once the response to the second service call is available, the function in the then() block is invoked. The response is searched to find the team ID needed for the next call, and myFetch() is called again to get all the players on the team.

  11. Enter the next then() block to acquire the list of players on the team, which is needed to then query the honors of each player in turn:

        .then(playerData => {

    The browser (and JavaScript runtime) is more than capable of handling the invocation of multiple service calls simultaneously.

    Note

    A naive approach would be to invoke all the service calls in a serial or synchronous fashion one after another, but doing this would cause the browser (or JavaScript runtime) to lock up until all the service calls are done since JavaScript has a single-threaded model.

  12. The map() function is called on the playerData.player list, which results in the list being iterated and a myFetch() call being invoked on each player on the list, and, hence, a number of new REST calls to TheSportsDB API. The resulting promises from each service call are collected in the honorRequests variable:

          const honorReqests = playerData.player.map(player =>

              myFetch(PLAYER_HONORS_URL, {id: player.idPlayer}));

  13. The Promise.all() method waits for all the service calls to complete before the associated promises are returned to be processed in the next then() block. Once available, the promises are returned as an array in the order the service calls were invoked. This array is iterated upon via forEach() to call printHonors() for each response:

          return Promise.all(honorReqests);

      })

      .then(honorResponses => honorResponses.forEach(printHonors))

  14. Finally, there is a catch() method in case errors occur during the processing of the promise:

    .catch(error => console.log(error));

    This simply logs the error to the console (in a real application, you should consider somehow indicating to the user that an error occurred, such as by showing an error message in the UI).

  15. Closeout the file with the following:

    </script>

    </body>

    </html>:

The code from the exercise has resulted in the browser such as the following:

Figure 17.1: Sample output of player honors

Figure 15.1: Sample output of player honors

In this exercise, we refactored the code to use the Fetch API. This was processed differently. TheSportsDB API only offers a service call to retrieve the honors for one player at a time. Consequently, to get the honors of all players on the team, you need to invoke many service calls, one for each player on the team. Here's where asynchrony comes in handy. Thus, the browser (and JavaScript runtime) is more than capable of handling the invocation of multiple service calls simultaneously. We will improve this in the next section.

An Improvement for Better Performance

The preceding code works, but there is still another improvement worth making. As a consequence of using Promise.all(), no results will display until all the requests to get player honors have returned. This produces a pause longer than necessary when loading the list.

You can improve perceived performance if you begin to display the list entries the moment the honor data of the first player is available, then the honors of the second player, and so on. You can do this even if the data for the rest of the players have not arrived yet, as long as you maintain the correct player order while displaying the list of entries.

To accomplish this, the basic approach is to create a promise to which a sequence of events are attached. You would take the promises returned by myFetch() for the respective players and attach them one by one to the sequence, as in the following pseudo-code:

promise

  .then(createPromiseForPlayer1())

  .then(printDataForPlayer1())     // print once data for player 1 is loaded

  .then(createPromiseForPlayer2()) // etc

  .then(printDataForPlayer2())

  .then(createPromiseForPlayer3())

  .then(printDataForPlayer3())

  .then(createPromiseForPlayer4())

  .then(printDataForPlayer4())

Our actual implementation will use forEach() to loop over the players to add them to the sequence. The promise for the sequence itself is created with Promise.resolve(), which results in a promise that resolves right away with no return value. But that's fine since this promise just serves as a placeholder to chain other items with a series of then() calls.

The earlier code that looked like this:

 .then(playerData => {

      const honorReqests = playerData.player.map(player =>

          myFetch(PLAYER_HONORS_URL, {id: player.idPlayer}));

      return Promise.all(honorReqests);

  })

  .then(honorResponses => honorResponses.forEach(printHonors))

  .catch(error => console.log(error));

Is now replaced with the following. You can find the code on GitHub in the file other/fetch_example_improved.html

  .then(playerData => {

      const sequence = Promise.resolve();

 

      playerData.player.forEach(player =>

          sequence

            .then(() => myFetch(PLAYER_HONORS_URL, {id: player.idPlayer}))

            .then(printHonors));

      return sequence;

  })

  .catch(error => console.log(error));

The resulting sequence will end up with a chain of then() clauses to fetch and print the honor data for each player, in the manner explained in the preceding pseudo-code.

For those more inclined to functional-style code, here is an alternate implementation. I'll let you decide which one of the two is more straightforward and clear.

  .then(playerData =>playerData.player.reduce((sequence, player) =>

          sequence

            .then(() =>myFetch(PLAYER_HONORS_URL, {id: player.idPlayer}))

            .then(printHonors)

    , Promise.resolve())

  )

Tidying Up Fetch Code

The preceding code presented to process promises with the then() and catch() methods executes correctly but is admittedly rather verbose and unwieldy. Could we do better? Let's try to make each then() and catch() into one-liners by refactoring the processing of each block into its own method.

The following code replaces the promise code that starts with myFetch() contained above. You can find the code on GitHub in the file other/fetch_tidied.html

myFetch(ALL_LEAGUES_URL)

  .then(leagueData =>getTeamsInLeague(leagueData, LEAGUE_NAME))

  .then(teamData =>getPlayersOnTeam(teamData, TEAM_NAME))

  .then(playerData =>getPlayerHonors(playerData))

  .catch(console.log);

Note how much more clean the code now reads, and you can more clearly see the progression of what the code is doing just from how the functions are named (that is, first get all leagues, then the correct league, then the right team, and so on).

The supporting functions you need to add are as follows. These basically have the code that was formerly in each corresponding then() block, which is restructured into their own functions:

function getTeamsInLeague(leagueData, leagueName) {

    const leagueId = findLeagueId(leagueData, leagueName);

    return myFetch(ALL_TEAMS_URL, {id: leagueId});

}

Function getPlayersOnTeam(teamData, teamName) {

    const teamId = findTeamId(teamData, teamName);

    return myFetch(ALL_PLAYERS_URL, {id: teamId});

}

function getPlayerHonors(playerData) {

    const sequence = Promise.resolve();

    playerData.player.forEach(player =>

        sequence

          .then(() => myFetch(PLAYER_HONORS_URL, {id: player.idPlayer}))

          .then(printHonors));

    return sequence;

}

Note

Stay tuned for the activity at the end of the chapter, where this code will be cleaned up and simplified even further using other advanced techniques such as currying.

Some Fetch API Usage Details

This section briefly summarizes some details of the Fetch API, which was introduced earlier.

Note

Some of the settings are noteworthy, but their full details are not within the scope of this chapter. These settings will be indicated since they might be important for you if your use case requires them.

The full method signature for the fetch() method is the following:

fetchResponsePromise = fetch(resource, init);

The init parameter allows you to assign certain custom settings to the request. Some of the available options include:

  • method: The request method, for example, GET and POST.
  • headers: Any headers that should be sent along with your request, contained within a Headers object (as shown in the following code snippet).
  • body: Anything, such as a string, Blob, or BufferSource, that you want to add to your request. Typically used for POST requests.
  • credentials: If the resource you are accessing requires credentials for authentication/authorization, you would specify this setting. Possible values are omit, same-origin, and include (the full details of credentials are not within the scope of this chapter).
  • cache: The cache mode to be used for the request. Valid values are default, no-cache, no-store, reload, force-cache, and only-if-cached (the full details of caching are not within the scope of this chapter).

    An example usage for a POST request is as follows:

    const url = "http://mysite.com/myservice";

    const data = {param1: 1234};

    let responsePromise = fetch(url, {

        method: 'POST',

        headers: {

            'Content-Type': 'application/json',

        },

        body: JSON.stringify(data)

    })

    .then(response => response.json());

    The fetch() method returns a promise that resolves to a response object that represents details pertaining to the response returned from the request. The following are the most important properties of the response object; all the properties are read-only:

  • Response.headers: Contains the headers associated with the response as an object with key-value pairs. The headers object contains methods to access them, such as using Headers.get() to retrieve the value for a given key, or Headers.forEach() to iterate over the key/value entries and call a function for each; for example:

    var headerVal = response.get("Content-Type");

          // application/json

    response.headers.forEach((val, key) => { 

        console.log(key, val); 

    });

    Note

    For cross-domain requests, there are restrictions on what headers are visible.

  • Response.ok: A Boolean indicating whether the response was successful. A response is considered successful if the status code is in the range of 200-299.
  • Response.status: The status code of the response, such as 404 to indicate a Not Found error.
  • Response.statusText: The status message text that corresponds to the status code, such as OK for 200.

In this section, you were introduced to promises and how they are used in the Fetch API. You saw how to retrieve remote data and how to handle errors.

Some developers feel that Fetch is a bit low-level and prefer other alternatives for remote requests. One popular library is the Axios library. As an example, where they feel Fetch is not ideal, Axios automatically transforms JSON responses to objects, whereas the transformation must be done explicitly in Fetch. There are also differences as to what statuses are considered errors to be handled in the catch() blocks (as Fetch only considers status codes of 500 or above to be errors, but for many use cases, any status code that is not 200 should be an error condition).

In most cases, there is no need to introduce another dependency into our code. The shortcomings mentioned can be overcome by creating simple wrappers around Fetch specific for your use cases, such as how you implemented the myFetch() wrapper function. Accessing the API though the wrapper offers most of the same functionality Axios would provide, however, you have more control.

In the next section, you will explore promises in detail.

Some Details Concerning Promises

You will now dig into the details of what promises are and how they are used in general, not necessarily in the context of service calls.

The constructor of a promise looks like this:

new Promise(function(resolve, reject) {

});

You would pass in an executor function that takes two arguments: resolve and (optionally) reject. When the promise is instantiated, this function is executed immediately. Your implementation of the executor function would typically initiate some asynchronous operation. Once the return value is available, it should then call the passed-in resolve function or reject if there is an error or other invalid condition. If an error is thrown in the executor function, it also causes the promise to be rejected (even if reject is not called explicitly).

Put into pseudo-code, this is similar to the following:

const promise = new Promise((resolve, reject) => {

    // do something asynchronous, which eventually calls either:

    //   resolve(someValue);  // fulfilled

    // or

  //   reject("failure reason");  // rejected

});

A promise can be in one of three possible states: fulfilled, rejected, or pending (not yet fulfilled or rejected). A promise is said to be settled once it is no longer in the pending state (either fulfilled or rejected).

As a simple example, consider a promise whose purpose is to introduce a deliberate 3-second delay to your processing. You could implement this using setTimeout() as follows:

const timeoutPromise = new Promise((resolve, reject) => { 

    setTimeout(() => {

        // call resolve() to signal that async operation is complete

        resolve("Called after three seconds!");

    }, 3000);

});

timeoutPromise.then(console.log);

This would result in the message Called after three seconds printing to the console. Note that reject() is not explicitly called in this instance (and the reject parameter can actually even be omitted if you wish).

Now for some details concerning what happens depending on the return value of the executor function. If the function:

  • Returns a value: The promise returned by then gets resolved with the returned value as its value.
  • Doesn't return anything: The promise returned by then gets resolved with an undefined value.
  • Throws an error: The promise returned by then gets rejected with the thrown error as its value.

Exercise 15.03: Creating a Utility Function to Delay Execution

In this exercise, you will produce a utility function for the creation of a promise to add a delay after another promise completes before the execution proceeds. This can be used if you want to do an async operation such as a service call, but do not want to process the result right away. This function will then be tested by making a service call and printing the result after a delay:

  1. In the Google Chrome browser, go into Developer Tools (the menu with three dots at the upper-right corner of the screen) | More Tools | Developer Tools, or just hit the F12 key).
  2. In the Console tab, paste in the following and hit Enter: You can find the code on GitHub in the file location: https://packt.live/2XM98vE

    function addDelay(ms, promise) {

        return promise.then(returnVal =>

            new Promise(resolve =>

                setTimeout(() => resolve(returnVal), ms)

            )

        );

    }

    This is our first attempt at a solution for this simple case, and the implementation resembles the preceding timeoutPromise code.

  3. You will test it by calling the service in TheSportsDB that gets the next event for a league and print the result to the console (the league ID is hardcoded in the URL for the purposes of this test). Paste the following code into the console and hit Enter.

    const BASE_URL = "https://www.thesportsdb.com/api/v1/json/1/";

    constnextEventUrl = BASE_URL + "eventsnextleague.php?id=4328";

    addDelay(3000, fetch(nextEventUrl))

      .then(response =>response.json())

      .then(nextEvents => console.log(nextEvents.events[0].strEvent));

    The preceding code results in the message Bournemouth vs Norwich in the console after 3 seconds, though your event will likely be different.

    Note

    You could have used the more robust myFetch() wrapper from the previous sections rather than fetch() as well.

Figure 17.2: Screenshot of result

Figure 15.2: Screenshot of the result

In this exercise, we learned how to add a delay to processing using the addDelay() function. This can be used if you want to do an async operation such as a service call, but do not want to process the result right away. In the next section, we will refine this function further.

Further Refinements to addDelay()

Now, by way of a bonus, let's see if you can think of different use cases for the addDelay() utility function presented in the preceding exercise, and how you can specify different parameter options to support these use cases.

The code in the preceding exercise works fine, but what if you wanted to make it more seamless and simply introduce a delay instruction as one of the then() clauses? For example:

fetch(nextEventUrl)

  .then(addDelay(1000))

  .then(response =>response.json())

  .then(nextEvents => console.log(nextEvents.events[1].strEvent));

This form is a bit cleaner and easier to see the flow (that is, fetch the response, add a delay of 1 second, and then process).

In order to support this, you now have two ways in which the parameters can be specified:

  • If two parameters are present, this is a simple case and a promise is returned that completes when the delay is over.
  • If only one parameter is present, there was no promise passed in at all. Here, rather than returning a promise, you will return a function that takes the promise as a parameter, with the expectation that the then() invocation will supply the promise when invoking the function. This function then makes a recursive call to the same addDelay() function with two parameters.

Our code now becomes the following:

function addDelay(ms, promise) {

    if (promise === undefined) {

        // In this case, only one param was specified.  Since you don't have

        // the promise yet, return a function with the promise as a param and

        // call addDelay() recursively with two params

        return promise =>addDelay(ms, promise);

    }

    // if you reached this far, there were two parameters

    return promise.then(returnVal =>

        new Promise(resolve =>

setTimeout(() => resolve(returnVal), ms)

        )

    );

}

There is one other use case you should consider that would make the utility function even more versatile. Let's say you don't start out with a promise at all and just want to return a value after a delay.

You can support this by calling Promise.resolve() with the value to convert it to a promise, which essentially treats it as an immediately fulfilled promise for that value. In the case that the value is already a promise, this call would have no effect.

Note

Calling Promise.resolve() on promise parameters is mentioned as a best practice anyway in the promise specification guide.

In general, when an argument is expected to be a promise, you should also allow thenables and non-promise values by resolving the argument to a promise before using it. You should never do type detection on the incoming value, overload between promises and other values, or put promises in a union type.

The final code looks like the following. You can find the code on GitHub in the file other/addDelay.js

function addDelay(ms, promise) {

    if (promise === undefined) {

In this case, only one parameter was specified. Since you don't have the promise yet, return a function with the promise as a parameter and call addDelay() recursively with two parameters:

        return promise =>addDelay(ms, promise);

    }

If you reached this far, there were two parameters:

    return Promise.resolve(promise).then(returnVal =>

        new Promise(resolve =>

setTimeout(() => resolve(returnVal), ms)

        )

    );

}

And here's the code to test the three scenarios:

const BASE_URL = "https://www.thesportsdb.com/api/v1/json/1/";

const nextEventUrl = BASE_URL + "eventsnextleague.php?id=4328";

Use case one is where two parameters are specified, so it executes a promise after a delay:

let p1 = addDelay(3000, fetch(nextEventUrl))

  .then(response => response.json())

  .then(nextEvents => console.log("Use 1: " + nextEvents.events[0].strEvent));

Use case two is where only one parameter is specified, so it returns a function that takes the promise as a parameter with the expectation that the then() invocation will supply the promise when invoking the function:

let p2 = fetch(nextEventUrl)

  .then(addDelay(1000))

  .then(response =>response.json())

  .then(nextEvents => console.log("Use 2: " + nextEvents.events[1].strEvent));

Use case three is where we just want to return a value after a delay:

let p3 = addDelay(2000, "This is a passed in value")

  .then(result => console.log("Use 3: " + result));

The output All done! should be written as follows:

Promise.all([p1, p2, p3])

  .then(() => console.log("All done!"));

The order of the output from the preceding code would be as follows:

    Use 2    (after 1 second)

    Use 3    (after 2 seconds)

    Use 1    (after 3 seconds)

The expected output will be as follows:

Figure 17.3: Screenshot of output

Figure 17.3: Screenshot of output

Remember, this is not a sequential code, even though it reads that way. It is important to wrap your head around this when working with asynchronous logic:

  • When the code that sets up Use 1 executes, it schedules the function to be called back after three seconds. But the main thread of execution continues immediately to set up Use 2 and does not wait for 3 seconds to complete.
  • Use 2 is then scheduled for 1 second in the future and will end up being triggered way before Use 1, so it is output first. Before this even happens, though, once again, the main thread of execution continues immediately to Use 3.
  • Use 3 is then scheduled for 2 seconds in the future. This is the second one to trigger and produce output, as Use 1 won't trigger until 3 seconds have passed.
  • Finally, Use 1 triggers and outputs when the third second is reached:
Figure 17.4: Use cases shown in a diagram

Figure 15.4: Use cases shown in a diagram

In this section, you learned the details of how promises are created and used. Promises have become an important part of JavaScript and many libraries and APIs use them. Promises have also become a basis for extending the language further and supporting them directly with new keywords, as you will soon see.

The next section will explore async/await, which expands the use of promises with a new syntax.

Async/Await

New additions to recent versions of JavaScript (since ES2017-ES8) make working with asynchronous logic easier, more transparent, and result in your code looking almost as if it were synchronous. This is the async/await syntax, which is one of the most exciting and useful additions to the language in recent years. We'll just dive right in and get a feel for how the async and await keywords are used by way of an example.

We will now present the changes you would make to refactor the promise code as you left it in the Further Refinements to addDelay() section to use async/await instead. Firstly, recall the main processing code that looked like this:

myFetch(ALL_LEAGUES_URL)

  .then(leagueData => getTeamsInLeague(leagueData, LEAGUE_NAME))

  .then(teamData => getPlayersOnTeam(teamData, TEAM_NAME))

  .then(playerData => getPlayerHonors(playerData))

  .catch(console.log)

When refactored to use the await syntax, it will look like the following. You can find the code on GitHub in the file other/async_await.html

    try {

        let leagueData = await myFetch(ALL_LEAGUES_URL);

        let teamData = await getTeamsInLeague(leagueData, LEAGUE_NAME);

        let playerData = await getPlayersOnTeam(teamData, TEAM_NAME);

        await getPlayerHonors(playerData);

    } catch (err) {

        console.log("caught error: " + err);

    }

The await keyword indicates that the function that follows returns a promise, and signals to the browser or JavaScript runtime to wait until the promise resolves and returns a result.

Using await is really just syntactic sugar as an alternative to calling promise.then(), and the result is the same as the value that would be passed as a parameter if promise.then() were called. But using await allows you to capture the result in a variable and looks as if you were writing synchronous code.

Also, notice how error handling is done using a typical try...catch block rather than a catch() function. This is another way in which await enables asynchronous code to be more seamless.

Another method we will refactor is myFetch(). Previously, it looked like this:

function myFetch(url, params) {

    if (params) {

        url += "?" + encodeParams(params);

    }

    return fetch(url)

        .then(response => {

            if (!response.ok) {

                throw new Error(response.status);

            }

            return response.json()

        }

    );

}

Refactored, it will now look like this:

async function myFetch(url, params) {

    if (params) {

        url += "?" + encodeParams(params);

    } 

    let response = await fetch(url);

    if (!response.ok) {

        throw new Error(response.status);

    }

    return response.json()

}

The async keyword before the function definition indicates that the function always returns a promise. Even if the actual value the function returns is not a promise, JavaScript will take care of wrapping that value in a promise automatically. In this case, the return value is the object resulting from the response.json() call, but what actually gets returned is a promise that wraps this. (The await keyword on the caller end would typically be used to unwrap the value again, but there are use cases where there is a need to work with the promise directly as well.)

Also notice how the fetch() function call now has an await keyword in front of it, rather than processing it utilizing the typical promise API with then().

There is another function you can refactor as well from a previous section: getPlayerHonors(). This is what it looked like before:

function getPlayerHonors(playerData) {

   const sequence = Promise.resolve();

    playerData.player.forEach(player =>

        sequence

          .then(() => myFetch(PLAYER_HONORS_URL, {id: player.idPlayer}))

          .then(printHonors));

    return sequence;

}

Remember that the purpose of this code is to make a REST service call to get player honor data for multiple players. Refactoring the code to use async/await, you can simplify it a bit and remove the sequence. Here's the new code:

async function getPlayerHonors(playerData) {

    const playerPromises = playerData.player.map(async player =>

        myFetch(PLAYER_HONORS_URL, {id: player.idPlayer}));

    for (constplayerPromise of playerPromises) {

        printHonors(await playerPromise);

    }

}

The array.map() function affects an iteration of all the players and calls myFetch() for each to get the honor data, resulting in an array of promises. Notice that you used the async keyword on the left of the arrow function. This is perfectly valid and just signals to array.map() that the function returns a promise. During processing, the execution of array.map() will not wait for the first function to complete before calling the next one. This makes the technique of utilizing array.map() with async well suited for launching concurrent requests.

Afterward, there is a second iteration using a standard for...loop, this time, of the promises produced earlier. The await keyword when calling printHonors would result in the execution waiting until the promise resolves before printing the available result. Also, since you are in a loop, you ensure the output is printed in the correct order.

Note

There is another important caveat to be aware of when using the await keyword: it only works if it is used within a function that is marked with the async keyword in front of it. Attempting to use it in a regular function or in top-level code will result in a syntax error. (For this reason, in the code that follows, notice that you will place the main processing code in an anonymous async function.)

Asynchronous Generators and Iterators

There is another implementation technique for the preceding getPlayerHonors() function to consider using it. This makes use of generator functions, which were described in Chapter 5, Beyond the Fundamentals. Generators, in general, are a recent and rather complex addition to the JavaScript language, and iterators are even newer, so not all browsers and runtime environments support them yet. We will therefore not spend a lot of time explaining them. But we just want to touch on them and explain very briefly how generators and iterators could be used with async.

Here's the implementation. You can fnd the code on GitHub in the file other/async_generator_impl.html

async function* getPlayerHonorsGenerator(playerData) {

 

    const playerPromises = playerData.player.map(async player =>

        myFetch(PLAYER_HONORS_URL, {id: player.idPlayer}));

 

    for (const playerPromise of playerPromises) {

        yield playerPromise;

    }

}

async function getPlayerHonors(playerData) {

    for await(const player of getPlayerHonorsGenerator(playerData)) {

        printHonors(player);

    }

}

The first getPlayerHonorsGenerator() function should look mostly familiar, as it is similar to the previous implementation, but with some important differences. The asterisk (*) that follows the function keyword indicates that it is a generator function, which means it returns multiple values via subsequent calls.

Notice the yield keyword in the loop. When yield is reached, execution passes back to the caller (which is actually the second function). When the generator function is called again, the execution picks up from where it left off in the middle of the loop and returns the next value. Once the loop ends, all the values have been returned, and the generator signals that it is done.

The second function calls the generator function using the for-await...of iterator syntax. The await keyword right after for makes it an async iterator. While performing the iteration, the execution will wait for each promise returned by the generator (via yield) to resolve in turn before executing the body of the loop.

Generators are a complex topic. However, by adopting this technique, you are able to access the results of multiple asynchronous calls in a clean looping syntax.

Activity 15.01: Refactoring Promise Code to await/async Syntax

Over the course of this chapter, you have explored how to take synchronous code and refactor it to use callbacks, promises, and async/await syntax. This activity will tie up some loose ends and challenge you to make some aspects of the code even better, partially by using the skills you learned in previous chapters.

The steps for completion are as follows:

  1. Firstly, recall the following code from Exercise 15.03, Creating a Utility Function to Delay Execution of this chapter, which uses promises to test three different uses of our addDelay function.
  2. Rewrite it to use the async/await syntax.
  3. For the purposes of this activity, you are not permitted to use Promise.all() (even though, in normal programming, it would be a good way to wait for the completion of multiple promises).

    Hint

    Be careful where you place your await keywords, as the three cases do not resolve in order.

The expected output is:

Figure 17.5: Sample output of player honors

Figure 17.5: Sample output of player honors

Note

The solution to this activity can be found on page 763.

Before we move on to the next activity, we will review briefly what currying is. Currying is taking a function with multiple arguments and breaking it down into one or more additional functions that take just one argument and eventually resolve to a value. The initial function call does not take all the arguments but returns a function whose input is the remaining arguments and whose output is the intended result of all the arguments.

Activity 15.02: Further Simplifying Promise Code to Remove Function Parameters

Shifting back to promises, we concluded the async/await section by tidying up the promise code to make the then() clauses one-liners. Here's the code again to refresh your memory:

myFetch(ALL_LEAGUES_URL)

  .then(leagueData =>getTeamsInLeague(leagueData, LEAGUE_NAME))

  .then(teamData =>getPlayersOnTeam(teamData, TEAM_NAME))

  .then(playerData =>getPlayerHonors(playerData))

  .catch(console.log)

This is pretty good, but could you do even better?

Now, we need to think of a way to simplify the code and remove the function parameters entirely, so it would look like this:

myFetch(ALL_LEAGUES_URL)

  .then(getTeamsInLeague(LEAGUE_NAME))

  .then(getPlayersOnTeam(TEAM_NAME))

  .then(getPlayerHonors)

  .catch(console.log)

Hint

Think about how you might defer the processing of the first parameter of getTeamsInLeague() and getPlayersOnTeam(). Refactor those functions to return another function that finally processes this parameter instead using currying techniques, which you learned about in Chapter 14, Understanding Functional Programming.

The original code is repeated here for your convenience (the getPlayerHonors() function already takes only one parameter and, therefore, has no need to be further simplified for this purpose):

function getTeamsInLeague(leagueData, leagueName) {

constleagueId = findLeagueId(leagueData, leagueName);

    return myFetch(ALL_TEAMS_URL, {id: leagueId});

}

function getPlayersOnTeam(teamData, teamName) {

constteamId = findTeamId(teamData, teamName);

    return myFetch(ALL_PLAYERS_URL, {id: teamId});

}

The steps for completion are as follows:

  1. In technique #1, refactor getTeamsInLeague so that it now only takes one parameter, (leagueName), rather than two parameters that are actually needed to determine the full result (leagueData, leagueName). The other parameter is deferred till later.
  2. In technique #1, instead of returning the promise from myFetch directly, you return another curried function that takes leagueData as its parameter. It is only a partially applied function at this point.
  3. Technique #2 is really the same idea but uses a function variable and multiple levels of arrow functions rather than a regular function.
  4. Finally, when getTeamsInLeague(LEAGUE_NAME) is invoked in the then() clause, the function returned above would be fully applied, with the resolved value from the previous promise passed in as the implied leagueData parameter.
  5. The process when calling getTeamsInLeague(LEAGUE_NAME) is incomplete at that point and returns another function to complete it. So, call a partially applied function.

    The expected output of the activity is the same as in Exercise 15.02, Refactoring the Honors List to Use the Fetch API, which gives a sample output of player honors.

    Note

    The solution to this activity can be found on page 765.

Summary

Like promises, async/await has become very important in JavaScript. You saw how this syntax helps your code appear almost like a synchronous code and can make your code clearer with regard to your desired intent. It even enables error handling in a more standard way with try/catch.

But this is sometimes deceptive and can get you into trouble if you are not careful. It is important to understand how an asynchronous code differs from sequential code, in particular, how asynchronous code is triggered by event loops and does not block the main execution thread. The same is true with promises themselves, but with async/await looking so similar to synchronous code, it could be easy to forget this fact.

That said, async/await is still very powerful and worth using. We have reached the end of this book. By now, you have gained a comprehensive understanding of the foundations and basics of JavaScript. You have also fully understood JavaScript syntax and structures for the web and beyond. Now, you are ready to build out intellectually challenging development problems to apply in everyday work.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.40.43