Async - Await in Javascript: Beginner's Guide

Async - Await in Javascript: Beginner's Guide

Introduction

Web development often requires us to manage asynchronous tasks. These are actions that we have to wait for while we move on to more important things. Requests to databases and networks are made. JavaScript is not blocking: JavaScript does not stop the execution of code as it waits. Instead, JavaScript uses an Event-Loop that allows JavaScript to execute other tasks efficiently while waiting for the completion of the asynchronous actions.

JavaScript originally used callbacks to manage asynchronous tasks. Callbacks can lead to complex, nested code that is difficult to debug and read. JavaScript has integrated native promises into ES6, which allows us to create much more readable code. JavaScript continues to improve, and ES8 offers a new syntax that allows us to handle our async...await asynchronous action. Async...await allows you to create asynchronous code that is similar to traditional, synchronous imperative programs.

The syntax of async...await is syntax sugar. It doesn't add new functionality to the language but introduces new syntaxes for using promises and generators. These were built into the language. Async...await dramatically improves the readability and scalability of the code. Let's find out how it works!

The Async Keyword

The async keyword is used to write functions that handle asynchronous actions. We wrap our asynchronous logic inside a function prepended with the async keyword. Then, we invoke that function.

async function myFunc() {
  // Function body here
};
myFunc();

We can also use async function expression

const myFunc = async () => {
  // Function body here
};
myFunc();

async functions always return a promise. This means we can use traditional promise syntax, like .then() and .catch, with our async functions. An async function will return in one of three ways:

  • If there’s nothing returned from the function, it will return a promise with a resolved value of undefined.

  • If there’s a non-promise value returned from the function, it will return a promise resolved to that value.

  • If a promise is returned from the function, it will simply return that promise.

async function showMsg(){
  return 'This is a Async Message'
}
showMsg().then((data) => console.log(data)) // This is a Async Message

The Await Operator

Only an async operation can use the await keyword. The operator await returns the value of a promise. Promises are not resolved in a fixed amount of time. Therefore, we need to wait for pauses or halts before our async function is executed until the promise has been fulfilled.

Most situations involve promises made from functions. It is possible to wait for the promise that it returns within an async operation. The function myPromise() returns a promise that will be resolved to the string "I'm now a resolved value!"

function myPromise(){
  return "I'm now a resolved value!"
}

async function myPromiseExample() {
  let resolvedValue = await myPromise()
  console.log(resolvedValue)
}
myPromiseExample() // "I'm now a resolved value!"

Within our async function, myPromiseExample(), we use await to halt our execution until myPromise() is resolved and assign its resolved value to the variable resolvedValue. Then we log resolvedValue to the console. We’re able to handle the logic for a promise in a way that reads like synchronous code.

Async Functions

As we've already seen, the await keyword stops an async operation until there is no promise. Do not forget to include the await keyword Although it may not seem obvious, this is a mistake that can lead to unforeseen consequences. Our function will continue running but won't produce the expected results.

This function returns the promise "I’m a resolved value!" After a one second delay:


const myProm = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("I'm a resolved value")
    }, 1000)
  })
}
// Writing async-await to resolve it
async function noAwait(){
  let value = myProm()
  console.log(value)
}
async function yesAwait(){
  let value = await myProm()
  console.log(value)
}
noAwait()
yesAwait()

In the first async function, noAwait(), we left off the await keyword before myPromise(). In the second, yesAwait(), we included it. The noAwait() function logs Promise { } to the console. Without the await keyword, the function execution wasn't paused. The console.log() on the following line was executed before the promise had resolved.

Remember that the await operator returns the resolved value of a promise. When used properly in yesAwait(), the variable value was assigned the resolved value of the myPromise() promise, whereas in noAwait(), value was assigned the promise object itself.

Handling Dependent Promises

Async...await's true beauty is when we have multiple asynchronous actions that depend on each other. We might make a network request using a query to the database. We would wait until the results of the database are available before making a network request. Native promise syntax uses a series of.then() function chains, making sure that each return is correct.

function nativePromiseVersion() {
  returnsFirstPromise()
    .then((firstValue) => {
      console.log(firstValue);
      return returnsSecondPromise(firstValue);
    })
   .then((secondValue) => {
      console.log(secondValue);
    });
}

Let’s break down what’s happening in the nativePromiseVersion() function:

  • Within our function we use two functions which return promises: returnsFirstPromise() and returnsSecondPromise().

  • We invoke returnsFirstPromise() and ensure that the first promise resolved by using .then().

  • In the callback of our first .then(), we log the resolved value of the first promise, firstValue, and then return returnsSecondPromise(firstValue).

  • We use another .then() to print the second promise’s resolved value to the console.

Here’s how we’d write an async function to accomplish the same thing:

async function asyncAwaitVersion() {
  let firstValue = await returnsFirstPromise();
  console.log(firstValue);
  let secondValue = await returnsSecondPromise(firstValue);
  console.log(secondValue);
}

Let’s break down what’s happening in our asyncAwaitVersion() function:

  • We mark our function as async.

  • Inside our function, we create a variable firstValue assigned await returnsFirstPromise(). This means firstValue is assigned the resolved value of the awaited promise.

  • Next, we log firstValue to the console.

  • Then, we create a variable secondValue assigned to await returnsSecondPromise(firstValue). Therefore, secondValue is assigned this promise’s resolved value.

  • Finally, we log secondValue to the console.

Though using the async...await syntax can save us some typing, the length reduction isn’t the main point. Given the two versions of the function, the async...await version more closely resembles synchronous code, which helps developers maintain and debug their code. The async...await syntax also makes it easy to store and refer to resolved values from promises further back in our chain which is a much more difficult task with native promise syntax. Let’s create some async functions with multiple await statements!

Handling Errors

If .catch() has been used in conjunction with a promise chain that is long, it is impossible to determine where the error occurred. It can be difficult to debug.

Async...await uses try...catch statements to handle errors. This syntax allows us to catch both synchronous as well as asynchronous errors. It makes it easier to debug!

async function usingTryCatch() {
try {
   let resolveValue = await asyncFunction('thing that will fail');
   let secondValue = await secondAsyncFunction(resolveValue);
} catch (err) {
   // Catches any errors in the try block
   console.log(err);
}
}

usingTryCatch();

Remember, since async functions return promises we can still use native promise’s .catch() with an async function

async function usingPromiseCatch() {
   let resolveValue = await asyncFunction('thing that will fail');
}

let rejectedPromise = usingPromiseCatch();
rejectedPromise.catch((rejectValue) => {
console.log(rejectValue);
})

This is sometimes used in the global scope to catch final errors in complex code.

Handling Independent Promises

Keep in mind that waiting halts the execution of the async functions. We can now write simple synchronous code that handles dependent promises. What if the async function has multiple promises that are independent of each other to be executed?

async function waiting() {
const firstValue = await firstAsyncThing();
const secondValue = await secondAsyncThing();
console.log(firstValue, secondValue);
}

async function concurrent() {
const firstPromise = firstAsyncThing();
const secondPromise = secondAsyncThing();
console.log(await firstPromise, await secondPromise);
}

In the waiting() function, we pause our function until the first promise resolves, then we construct the second promise. Once that resolves, we print both resolved values to the console.

In our concurrent() function, both promises are constructed without using await. We then await each of their resolutions to print them to the console.

With our concurrent() function both promises’ asynchronous operations can be run simultaneously. If possible, we want to get started on each asynchronous operation as soon as possible! Within our async functions we should still take advantage of concurrency, the ability to perform asynchronous actions at the same time.

Note: if we have multiple truly independent promises that we would like to execute fully in parallel, we must use individual .then() functions and avoid halting our execution with await.

Await Promise.all()

Another way to take advantage of concurrency when we have multiple promises which can be executed simultaneously is to await a Promise.all().

We can pass an array of promises as the argument to Promise.all(), and it will return a single promise. This promise will resolve when all of the promises in the argument array have been resolved. This promise’s resolve value will be an array containing the resolved values of each promise from the argument array.

async function asyncPromAll() {
  const resultArray = await Promise.all([asyncTask1(), asyncTask2(), asyncTask3(), asyncTask4()]);
  for (let i = 0; i<resultArray.length; i++){
    console.log(resultArray[i]);
  }
}

In our above example, we await the resolution of a Promise.all(). This Promise.all() was invoked with an argument array containing four promises (returned from required-in functions). Next, we loop through our resultArray, and log each item to the console. The first element in resultArray is the resolved value of the asyncTask1() promise, the second is the value of the asyncTask2() promise, and so on.

Promise.all() allows us to take advantage of asynchronicity— each of the four asynchronous tasks can process concurrently. Promise.all() also has the benefit of failing fast, meaning it won’t wait for the rest of the asynchronous actions to complete once any one has been rejected. As soon as the first promise in the array rejects, the promise returned from Promise.all() will reject with that reason. As it was when working with native promises, Promise.all() is a good choice if multiple asynchronous tasks are all required, but none must wait for any other before executing.

In Conclusion

In conclusion, the async/await feature in JavaScript provides a more readable and manageable way to handle asynchronous operations. The async keyword is used to define a function as asynchronous, while the await operator is used to pause the execution of the function until the promise is resolved or rejected. Async functions can be used to wrap any promise-based function and return a promise.

Handling dependent promises is simplified with async/await, as we can use the await operator to wait for the resolution of one promise before proceeding with the next one. We can also use the Promise.all() method with await to wait for multiple promises to resolve or reject.

However, handling errors is still important in async/await. We can use try/catch blocks to handle errors that may occur during the execution of the asynchronous code. Additionally, we can use the catch() method to handle errors that occur during the execution of promises.

Overall, async/await is a powerful feature in JavaScript that simplifies the handling of asynchronous operations and makes code more readable and manageable.