Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
JavaScript Design Patterns

You're reading from   JavaScript Design Patterns Deliver fast and efficient production-grade JavaScript applications at scale

Arrow left icon
Product type Paperback
Published in Mar 2024
Publisher Packt
ISBN-13 9781804612279
Length 308 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Hugo Di Francesco Hugo Di Francesco
Author Profile Icon Hugo Di Francesco
Hugo Di Francesco
Arrow right icon
View More author details
Toc

Table of Contents (16) Chapters Close

Preface 1. Part 1:Design Patterns
2. Chapter 1: Working with Creational Design Patterns FREE CHAPTER 3. Chapter 2: Implementing Structural Design Patterns 4. Chapter 3: Leveraging Behavioral Design Patterns 5. Part 2:Architecture and UI Patterns
6. Chapter 4: Exploring Reactive View Library Patterns 7. Chapter 5: Rendering Strategies and Page Hydration 8. Chapter 6: Micro Frontends, Zones, and Islands Architectures 9. Part 3:Performance and Security Patterns
10. Chapter 7: Asynchronous Programming Performance Patterns 11. Chapter 8: Event-Driven Programming Patterns 12. Chapter 9: Maximizing Performance – Lazy Loading and Code Splitting 13. Chapter 10: Asset Loading Strategies and Executing Code off the Main Thread 14. Index 15. Other Books You May Enjoy

Controlling sequential asynchronous operations with async/await and Promises

Promises were introduced in ES2015 (ES6), along with other modern data structures.

For those familiar with JavaScript prior to ES2015, asynchronous behavior was modeled with callback-based interfaces, for example, request(url, (error, response) => { /* do work with response */ }). The key issues that Promises resolved were the chaining of asynchronous requests and issues around managing parallel requests, which we’ll cover in this section.

ES2016 included the initial specification for the async/await syntax. It built on top of the Promise object in order to write asynchronous code that didn’t involve “Promise chains,” where different Promises are processed using the Promise().then function. Promise functionality and async/await interoperate nicely. In fact, calling an async function returns a Promise.

We’ll start by showing how to use Promises to manage sequential asynchronous operations. We’ll use the Fetch API (which returns a Promise) to load fakestoreapi.com/auth/login. Given a username and password, and based on the output, we’ll load all the relevant carts for that user. Subsequently, we’ll load the relevant carts for that user using the fakestoreapi.com/carts/user/{userId} endpoint. This request flow is visualized in the following diagram.

Figure 7.1: Sequence of /auth/login and /carts/user/{userId} requests

Figure 7.1: Sequence of /auth/login and /carts/user/{userId} requests

We’ll start by sending a POST request to the auth/login endpoint. We add .then((res) => res.json()), which will wait for the initial fetch() output Promise to resolve to a “response” (hence the res name). We then call the .json() method on the response, which again is a Promise, which resolves to the JSON-decoded response body:

function fetchAuthUserThenCartsPromiseThen(username,
  password) {
  return fetch('https://fakestoreapi.com/auth/login', {
    method: 'POST',
    body: JSON.stringify({
      username,
      password,
    }),
    headers: {
      'Content-Type': 'application/json',
    },
  }).then((res) => res.json());
}

The Promise returned from res.json() can be accessed in another .then() callback, in which we parse the token field, which is a JSON Web Token (JWT), using the jwt-decode package.

We extract the sub field from the decoded token. This is the “subject” claim, which tells us which user this token is about. In the case of the fakestoreapi token, userId is used as the “subject” claim. We can therefore use the sub claim as the user ID for which to load the carts in our following API call to https://fakestoreapi.com/carts/user/{userId}:

import jwt_decode from 'https://esm.sh/jwt-decode';
function fetchAuthUserThenCartsPromiseThen(username,
  password) {
  return // no change to the fetch() call
    .then((res) => res.json())
    .then((responseData) => {
      const parsedValues = jwt_decode(responseData.token);
      const userId = parsedValues.sub;
      return userId;
    })
    .then((userId) =>
      fetch(`https://fakestoreapi.com/carts/user/${userId}
        ?sort=desc`)
    )
    .then((res) => res.json());
}

This function can then be used as follows. Note that a password shouldn’t be stored in the source of a production application (as it is in this example).

When we call the fetchAuthUserThenCartsPromiseThen function, it makes both the /auth/login call and then the /carts/user/{userId} call, which means we receive an array with the relevant carts for the requested user (note userId = 3, which is the correct ID for the kevinryan user).

Note that we’re using async/await here to “flatten” the Promise output into userCartsDataPromiseThen, which we can assert on:

const username = 'kevinryan';
const password = 'kev02937@';
const userCartsDataPromiseThen = await
  fetchAuthUserThenCartsPromiseThen(
  username,
  password
);
assert.deepEqual(userCartsDataPromiseThen, [
  {
    __v: 0,
    date: '2020-01-01T00:00:00.000Z',
    id: 4,
    products: [
      {
        productId: 1,
        quantity: 4,
      },
    ],
    userId: 3,
  },
  {
    __v: 0,
    date: '2020-03-01T00:00:00.000Z',
    id: 5,
    products: [
      {
        productId: 7,
        quantity: 1,
      },
      {
        productId: 8,
        quantity: 1,
      },
    ],
    userId: 3,
  },
]);

As we’ve just seen in the code that calls fetchAuthUserThenCartsPromiseThen, the key benefit of async/await over Promise().then() chains is that the code is structured more similarly to synchronous code.

In synchronous code, the output of an operation can be, for example, assigned to a constant:

const output = syncGetAuthUserCarts();
console.log(output);

Whereas with Promise().then(), the output is available only in an additional .then callback:

promisifiedGetAuthUserCarts().then((output) => {
  console.log(output);
});

What await allows us to do is to structure the code as follows:

const output = await promisifiedGetAuthUserCarts();
console.log(output);

One way to think of it is that await can unfurl Promises. A Promise’s “resolved value”, usually only accessible in a Promise().then() callback is available directly.

For sequential operations, this is very useful, since it makes the code structured with a set of variable assignments per async operation.

The await operator is available at the top level of ECMAScript modules in modern runtime environments as part of the ES2022 specification.

However, in order to use await inside of a function, we need to mark the function as async. This usage of await in async functions has been available since ES2016.

Code editors and IDEs such as Visual Studio Code provide a refactor from chained Promise().then() calls to async/await. In our case, we can build a fetchAuthUserThenCartsAsyncAwait function as follows.

Instead of using fetch().then(res => res.json()), we’ll first use await fetch() and then await authResponse.json():

async function fetchAuthUserThenCartsAsyncAwait
  (username, password) {
  const authResponse = await fetch('https://fakestoreapi.com/auth/login', {
    method: 'POST',
    body: JSON.stringify({
      username,
      password,
    }),
    headers: {
      'Content-Type': 'application/json',
    },
  });
  const authData = await authResponse.json();
}

We now have access to authData. We can decode authData.token as before using the jwt-decode package. This gives us access to the sub (subject) claim, which is the user ID:

Import jwt_decode from 'https://esm.sh/jwt-decode';
async function fetchAuthUserThenCartsAsyncAwait
  (username, password) {
  // no change to /auth/login API call code
  const parsedValues = jwt_decode(authData.token);
  const userId = parsedValues.sub;
}

Now that we have the relevant user ID, we can call the /carts/user/{userId} endpoint to load the user’s carts:

async function fetchAuthUserThenCartsAsyncAwait
  (username, password) {
  // no change to /auth/login call or token parsing logic
  const userCartsResponse = await fetch(
    `https://fakestoreapi.com/carts/user/${userId}?sort=desc`
  );
  const userCartsResponseData = await userCartsResponse.
    json();
  return userCartsResponseData;
}

Given the same input data as the approach using Promise().then(), the loaded carts are the same. Note, again, that passwords and credentials should not be stored in source code files:

const username = 'kevinryan';
const password = 'kev02937@';
const userCartsDataAsyncAwait = await fetchAuthUserThenCartsAsyncAwait(
  username,
  password
);
assert.deepEqual(userCartsDataAsyncAwait, userCartsDataPromiseThen);

One difference between the approaches is that with async/await, all the variables are defined in a single function scope, whereas the Promise().then() approach uses multiple function scopes (for each of the callbacks passed to .then()). With a single large function scope, variable names can’t clash, which makes the code more verbose since, for example, each response object needs a qualifier to avoid variable name clashes, for example, authResponse and userCartsResponse.

The benefit of a single larger function scope is that all the outputs of previous API calls are available to subsequent ones without having to explicitly set them as values passed as a return in the callback passed to .then().

Finally, a fetch()-specific example, is that since there are multiple Promises that require handling when doing a fetch and accessing the JSON response, the await approach can be a bit “noisier.”

See the two following samples. First, with async/await, we assign a variable for the fetch response value:

const response = await fetch(url);
const data = await response.json();

Next, with .then(), we assign only a data variable and use an arrow function to handle the .json() unfurling:

const data = await fetch(url).then((response) => response.json());

As you see, our final example is a mix of async/await and Promise().then() so that the most “important” parts of the code are obvious. The specifics of how we extract the JSON output from fetch are not necessarily core to our logic so might be better expressed with Promise().then().

In general, this slight difference in style wouldn’t occur since parts of the code that are “less important,” such as how we interact with the fetch API to process a request to JSON, tend to be abstracted – in this case, in an HTTP client of some kind. We would expect that the HTTP client could handle checking response.ok and accessing the response body as parsed JSON (using response.json()).

We’ve now seen how to implement sequential asynchronous operations using a Promise-only approach, an async/await-based approach, and finally, how both the async/await and Promise techniques can be used together to improve code readability and performance.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image