Basic asynchronous JavaScript: async/await vs promise chaining

Happy Valentine’s Day. I was going through some Scrimba material in the Frontend Developer Path because it was revamped last year and new content was added while old content was removed or placed in different areas (a big change was the Next-Level JavaScript unit being removed and new content being added to the Essential JavaScript unit). While doing so, I started comparing code from an old project that calls out to an API for some data to render. Requesting data from an API is an asynchronous function – the response that the function is waiting for is wrapped in an RSVP-like package called a promise.

Promises, promises…

A promise is “the eventual completion (or failure) of an asynchronous operation and its resulting value”. It’s often a response from an outside source, like an API, that fulfills a request. Essentially, some code in an application asks for data from the API and the API “promises” to get back to the requester. Sometimes, this response is a success – the data that was asked for is sent. Other times, it fails. In that case, an error object is sent with a message inside that (hopefully) explains what went wrong.

There are two different approaches (without involving outside libraries like axios) that I’m aware of for handling promises in JavaScript. The first is called promise chaining. It relies on two methods to operate: .then() and .catch(). The other approach is writing asynchronous functions using async and await.

Promise chaining

fetch("https://dog.ceo/api/breeds/image/random")
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error))

The above example uses a fetch() request to ask an API for some data. If the API responds with the data, the response is converted into JSON format using the .json() method and logged to the console. If it responds with an error, that error message is logged to the console instead. The above approach is called promise chaining.

fetch() is a built-in JavaScript function for making requests across a network. It returns a promise. The basic syntax is to provide it with a URL to an API endpoint to reach out to. The response from the API is then processed via a callback function using the .then() method. Errors are processed via another callback function using the .catch() method.

.then() can be used to change (mutate) the response and additional .then() methods can be called, one after another, to further work with the response. This is another example of promise chaining. .then() is commonly used with promises to handle asynchronous operations. It’s used to register callbacks that will be invoked when the promise is fulfilled (successfully resolved) or rejected (returning an error).

The .catch() method is used with promises to handle errors that occur with asynchronous operations. It’s used to define a callback function that will be invoked when a promise is rejected.

So, in the above example, a fetch() request is made and then two .then() actions are chained together. The first takes the API response and converts it to JSON. The second then takes that JSON data and logs it to the console. But why is this data called “response” in the first line and then “data” in the next? It’s because .json() ALSO returns a promise! It takes in “response’ and returns something else – in this case: “data”.

We can call the responses from the fetch() request and the .json() method anything we want, but a common convention is to call the data received from a fetch() request “response” and to call the data that was converted by .json() “data”. This would be equally acceptable, but it doesn’t follow convention:

fetch("https://dog.ceo/api/breeds/image/random")
.then(doggyData => doggyData.json())
.then(doggyJson => console.log(doggyJson))
.catch(error => console.error('Error:', error))

It’s interesting that most people don’t consider renaming the data in the callback function inside of the .catch() anything but “error”. If it ain’t broke…

Async / Await

async function logDog() {
const response = await fetch("https://dog.ceo/api/breeds/image/random")
const jsonData = await response.json()
console.log(jsonData)
}

The above code is the async/await equivalent of the promise chaining example from the previous section. The async keyword can be used to change any function into an asynchronous one that returns a promise. Any value returned by the function is wrapped in a resolved promise. await is used on the actual command that returns a promise, causing JavaScript to act synchronously and wait for the promise to resolve.

  • await can only be used inside of an async function
  • It causes the function to pause and wait for a resolved promise before continuing

Note that in this basic example, we’re not handling any errors (rejected promises) from the API. However, in this async version, response and jsonData are variables. This means that they can be manipulated like any other JavaScript variables. fetch() doesn’t allow for direct saving and manipulation in the same manner.

So, in the above example, the logDog() function is made asynchronous via the async keyword. Then the fetch() request is then made and its (hopefully successful) response is saved to a variable called “response”. That response data is then converted into JSON and saved to another variable called “jsonData”. Finally, the JSON object is logged to the console.

Waiting…

But – if a fetch() request returns a promise, and if .json() also returns a promise – why are we prefacing them with the await keyword? Isn’t that redundant?

In JavaScript, the await keyword is used to pause the execution of an async function until a promise is resolved. It doesn’t make the promise itself redundant – it allows you to work with promises in a more synchronous manner.

Even though both fetch() and .json() return promises, using await ensures that the code waits for the promises to resolve before proceeding. Without await, the subsequent lines of code would execute immediately after the promises are created. This can lead to “race conditions” or incorrect program behavior.

Comparison

So, what are the benefits of using async/await versus fetch() with promise chaining? They mainly differ in terms of coding style and readability. Both approaches achieve the same result but offer different syntaxes for handling asynchronous operations. Some considerations include:

  • Readability: Some developers find async/await more readable and easier to understand, especially when dealing with multiple asynchronous operations.
  • Error handling: async/await allows use of try/catch blocks for error handling, making it similar to synchronous error handling. With .then() chaining, error handling is typically done using .catch() at the end of the chain.
  • Chaining: With .then() chaining, its easy to chain multiple asynchronous operations together, which can be more concise than using async/await. (Note that error handling wasn’t incorporated into the async/await function!)
  • Compatibility: async/await might not be available in older browsers. fetch() with .then() is more widely supported.

Both approaches achieve the same result – fetching data from the API and logging it to the console. The choice between them comes down to personal preference, team conventions, and project requirements.

Leave a comment