Async Await in javascript

javascript Aug 02, 2019

Async await is a new syntax form added to simplify code that uses promises. Async await introduces two new keywords: async and await. Async is added to function declarations and await is used inside an async function. It is surprisingly easy to understand and use. In its simplest form, async await allows us to write promise-based asynchronous code that looks almost identical to the synchronous code that does the same task. We will use async/await to simplify code using promises and make it even easier to read and understand.

Async Await Syntax

The async keyword is added to function declarations; it must precede the function keyword. An async function declaration defines an asynchronous function. An example declaration of an async function is shown in the following snippet:

async function asyncExample( /* arguments */ ){ /* do work */ }
Snippet 2.34: Implementing promises

An async function implicitly returns a promise, no matter what the return value is specified to be. If the return value is specified as a non-promise type, JavaScript automatically creates a promise and resolves that promise with the returned value. This means that all async functions can have the Promise.then() and Promise.catch() handlers applied to the return value. This allows for very easy integration with existing promise-based code. This is shown in the following snippet:

async function example1( ){ return 'Hello'; }
async function example2( ){ return Promise.resolve( 'World' ); }
example1().then( console.log ); // Expected output: Hello
example2().then( console.log ); // Expected output: World

The await keyword can only be used inside of an async function. Await tells JavaScript to wait until the associated promise settles and returns its result. This means that JavaScript pauses execution of that block of code, waits for the promise to be resolved while doing other async work, then resumes that block of code once the promise settles. This makes the awaited block of code run like a synchronous function, but it does not cost any resources because the JavaScript engine can still do other work, such as run scripts or handle events, while the asynchronous code is being awaited. An example of the await keyword is shown in the following snippet.

Even though async/await functionality makes JavaScript code look and act as if it were synchronous, JavaScript is still running the code asynchronously with the event loop.
async function awaitExample( /* arguments */ ){ 
  let promise = new Promise( ( resolve, reject ) => {
    setTimeout( () => resolve( 'done!'), 100 );
  });
  const result = await promise;
  console.log( result ); // Expected output: done!
}
awaitExample( /* arguments */ );

In the preceding example, we defined an async function, awaitExample(). Since it is an async function, we can use the await keyword. Inside the function, we create a promise that does asynchronous work. In this case, it simply waits 100 milliseconds and then resolves the promise with the string done!. We then await the created promise. When the promise is resolved with a value, the await takes that value and returns it, and the value is saved in the variable result. We then log the value of result to the console. Instead of using a then handler on the promise to get the resolution value, we simply await the value. The await block of this code looks similar to a synchronous code block.

Asnyc/Await Promise Rejection

Now that we know how to handle promise fulfillment with async/await, how can we handle promise rejection? Error rejection with async/await is very simple and works fantastically with the standard JavaScript error handling. If a promise is rejected, the await statement waiting for that promise resolution throws an error. When an error is thrown inside an async function, it is caught automatically by the JavaScript engine and the promise returned by the async function is rejected with that error. This sounds slightly complicated, but it is very simple. These relations are shown in the following snippet:

async function errorExample1( /* arguments */ ){ 
  return Promise.reject( 'Rejected!' );
}
async function errorExample2( /* arguments */ ){ 
  throw 'Rejected!';
}
async function errorExample3( /* arguments */ ){ 
  await Promise.reject( 'Rejected!' );
}
errorExample1().catch( console.log ); // Expected output: Rejected!
errorExample2().catch( console.log ); // Expected output: Rejected!
errorExample3().catch( console.log ); // Expected output: Rejected!

In the preceding snippet, we created three async functions. In the first function, errorExample1(), we return a promise that is rejected with the string Rejected!. In the second function, errorExample2(), we throw the string Rejected!. Since this is an error thrown inside an async function, the async function wraps it in a promise and returns a promise rejected with the thrown value. In this case, it returns a promise rejected with the string Rejected!. In the third function, errorExmaple3, we await a rejected promise. Awaiting rejected promises causes JavaScript to throw the promise rejection value, which is Rejected!. The async function then catches the error thrown with this value,, wraps it in a promise, rejects the promise with that value, and returns the rejected promise. All three example functions return a promise rejected with the same value.

Since await throws an error if the awaited promise is rejected, we can simply use the standard try/catch error handling mechanism in JavaScript to handle the async errors. This is very useful because it allows us to handle all errors in the same manner, whether asynchronous or synchronous. This is shown in the following example:

async function tryCatchExample() {
// Try to do asynchronous work
  try{
    const value1 = await Promise.resolve( 'Success 1' );
    const value2 = await Promise.resolve( 'Success 2' );
    const value3 = await Promise.reject( 'Oh no!' );
  } 
  
  // Catch errors
  catch( err ){
    console.log( err ); // Expected output: Oh no!
  }
}
tryCatchExample()

In the preceding example, we created an async function that tries to do asynchronous work. The function tries to await three promises in a row. The final one is rejected, which causes an error to be thrown. This error is caught and handled by the catch block.

Since errors are wrapped in promises and rejected by async functions, and await throws errors when a promise is rejected, async/await function error propagate upwards to the highest level await call. This means that unless an error needs to be handled in a special manner at various nesting levels, we can simply use a single try catch block for the outermost error. The error will propagate up the async/await function stack through rejected promises, and only needs to be caught by the top level await block. This is shown in the following snippet:

async function nested1() { return await Promise.reject( 'Error!' ); }
async function nested2() { return await nested1; }
async function nested3() { return await nested2; }
async function nestedErrorExample() {
  try{ const value1 = await nested3; }
  catch( err ){ console.log( err ); } // Expected output: Oh no!
}
nestedErrorExample();

In the preceding example, we created several async functions that await the result of another async function. They are called in the order nextedErrorExample() -> nested3() -> nested2() -> nested1(). The body of nested1() awaits a rejected promise, which throws an error. Nested1() catches this error and returns a promise rejected with that error. The body of nested2() awaits the promise returned by nested1(). The promise returned by nested1() was rejected with the original error, so the await in nested2() throws an error, which is wrapped in a promise by nested2(). This propagates down until the await in nestedErrorExample(). The await in the nested error example throws an error, which is caught and handled. Since we only need to handle the error at the highest level, we put the try/catch block at the outermost await call and allow the error to propagate upward until it hits that try/catch block.

Using Async Await

Now that we know how to use async/await, we need to integrate it into our promise code. To convert our promise code to use async/await, we simply need to break the promise chains into async functions and await each step. The chain of promise handlers is separated at each handler function (then(), catch(), and so on). The value returned by the promise is caught with an await statement and saved into a variable. This value is then passed into the callback function of the first promise then() promise handler, and the result of the function should be caught with an await statement and saved into a new variable. This is done for each then() handler in the promise chain. To handle the errors and promise rejections, we surround the entire block with a try catch block. An example of this is shown in the following snippet:

/ Promise chain - API functions return a promise
myPromse.then( apiCall1 ).then( apiCall2 ).then( apiCall3 ).catch( errorHandler );
async function asyncAwaitUse( myPromise ) {
  try{
    const value1 = await myPromise;
    const value2 = await apiCall1( value1 );
    const value3 = await apiCall2( value2 );
    const value4 = await apiCall3( value3 );
  } catch( err ){
    errorHandler( err );
  }
}
asyncAwaitUse( myPromise );

As we can see in the promise chain, we chain three API calls and an error handler on to the resolution of myPromise. At each promise chain step, a promise is returned and a new Promise.then() handler is attached. If one of the promise chain steps is rejected, the catch handler is called.

In the async/await example, we break the promise chain at each Promise.then() handler. We then convert the then handlers into functions that return promises. In this case, apiCall1(), apiCall2(), and apiCall3() already return promises. We then await each API call step. To handle a promise rejection, we must surround the entire block with a try catch statement.

Much like with promise chains with multiple chained then handlers, an async function with multiple await calls will run each await call one at a time, not starting the next await call until the previous await call has received a value from the associated promise. This can slow down asynchronous work if we are trying to complete several asynchronous tasks at the same time. We must wait for each step to complete before starting the next step. To avoid this, we can use Promise.all with await.

As we learned earlier, Promise.all runs all the child promises at the same time and returns a pending promise that is not fulfilled until all of the child promises have been resolved with a value. We can await a Promise.all much like we would attach a then handler to a Promise.all. The value returned by an await Promise.all call will only be available when all the child promises have completed. This is shown in the following snippet:

async function awaitPromiseAll(){
  let promise1 = new Promise( ( resolve, reject ) => setTimeout( () => resolve( 10 ), 100 ) );
  let promise2 = new Promise( ( resolve, reject ) => setTimeout( () => resolve( 20 ), 200 ) );
  let promise3 = new Promise( ( resolve, reject ) => setTimeout( () => resolve( 30 ), 10 ) );
  const result = await Promise.all( [ promise1, promise2, promise3 ] );
  console.log( result ); //Expected output: [ 10, 20, 30 ]
}
awaitPromiseAll();

As we can see from the preceding example, we created several promises, pass those promises into a Promise.all call, then await the resolution of the promise returned by Promise.all. This follows the rules of async/await just as we would expect it to. This same logic can be applied to Promise.race as well.

An example of a promise race is shown in the following snippet:

async function awaitPromiseAll(){
  let promise1 = new Promise( ( resolve, reject ) => setTimeout( () => resolve( 10 ), 100 ) );
  let promise2 = new Promise( ( resolve, reject ) => setTimeout( () => resolve( 20 ), 200 ) );
  const result = await Promise.race( [ promise1, promise2 ] );
  console.log( result ); //Expected output: 10]
}
awaitPromiseAll();

Conclusion

Async/await is an amazing new syntax format that helps us simplify promise-based code. It allows us to write code that looks like synchronous code. Async/await introduces two keywords, async and await. Async is used to denote an async function. It prepends the function keyword when declaring functions. Async functions always return a promise. The await keyword can only be used inside async functions on promises. It tells the JavaScript engine to wait on a promise resolution, and on rejection or fulfillment, throws an error or returns the value. Async/await error handling is done through thrown errors and rejected promises. An async function automatically catches thrown errors and returns a promise rejected with that error. Awaited promises throw errors on rejection. This allows error handling to be coupled easily with the standard JavaScript try/catch error handling. Async/await is very easy to integrate into your promise-based code and can make it very easy to read.

Neeraj Dana

Experienced Software Engineer with a demonstrated history of working in the information technology and services industry. Skilled in Angular, React, React-Native, Vue js, Machine Learning