In this tutorial, we are going to be looking at how you can use both the async and await keywords within your TypeScript application and subsequently improve the readibility and succinctness of your code.

Introduction

If you have come from a Node.JS background, you’ll most likely be familiar with something we know as Callback Hell. This is where you end up with code within your Node.JS applications that look akin to the Pyramids from ancient Giza.

myAwesomeFunction(function(x){
    mySecondFunction(x, function(y){
        myThirdNestedFunction(y, function(z){ 
            // here we handle the results of the 3 chained functions.
        });
    });
});

People quickly realized that this was an issue and as such Promises were introduced to the language in order to address said issue. These promises are objects that will eventually return as complete or as failed and they allowed us to rewrite the above code like so:

// our original promise function
myAwesomeFunction()
  // on completion would trigger this
  .then(function(result) {
    // which would in turn return another promise
    return mySecondFunction(result);
  })
  // which on completion would trigger this
  .then(function(result2) {
    // which would again return a promise
    return myThirdNestedFunction(finalResult);
  })
  // before finally executing this block
  .then(function(finalResult) {
    // and in here we could play with the result
    // of all three promise calls
    console.log(finalResult)
  })
  // within here we could catch any errors 
  // in our above promises
  .catch(function(error) {
    console.log(error)
  });

Now, this was certainly an improvement, however, this isn’t all that much better and keeping track of the results of your nested function calls is painful.

Thankfully, this is what helped bring about both the async and the await keywords.

Async + Await Keywords

The async keyword within a TypeScript program lets us define an asynchronous function like so:

async function myAwesomeFunction() {
  setTimeout(() => {}, 100, 'foo');
}

const result = myAwesomeFunction();
console.log(result);
// returns Promise { undefined }

We can then call this asynchronous function in such a manner that its execution becomes synchronous.

Wait, why would we want that?

By doing this, we can write code that is far cleaner and more understandable than our chained promise example. It allows us to utilize the await keyword in front of an express that will return a promise and pause the execution of the function until the await-ed promise resolves with a result.

async function mySecondFunction(x: number) {
  setTimeout(() => {}, 100);
  return 2 + x;
}

async function myThirdNestedFunction(x: number) {
  setTimeout(() => {}, 100);
  return 3 + x;
}

async function myAwesomeFunction() {
  let startingValue = 1;
  // we can await the call to mySecondFunction() as this 
  // returns a promise that will eventually return
  // our firstResult
  let firstResult = await mySecondFunction(startingValue);
  // once mySecondFunction has resolve, our function will
  // carry on execution of myThirdNestedFunction
  let finalResult = await myThirdNestedFunction(firstResult);
  // once this resolves, we get back our finalResult
  // which we can subsequently return 
  return finalResult;
}

let promise = myAwesomeFunction();
promise.then((result) =>  console.log(result));

Now, this should instantly highlight just how much better your application’s code can be by switching out chained promises and instead using these new keywords!

The final result is far more terse, and far more readable for new developers coming into the codebase.

Mixing in Raw Promises

If you start working with the async and await keywords, you should note that you don’t preclude the use of raw Promises within your TypeScript applications.

The async keyword will still return a promise which can be used in conjunction with something like Promise.all(). This gives us the flexibility to run our asynchronous functions in an asynchronous manner whilst ensuring the execution within those functions remains synchronous. Confusing isn’t it?

Let’s take a look at what I mean by this in the code:

async function myAsyncFunction(input: number) {
  // the internal part of our async function
  // will still be executed synchronously thanks
  // to the await keyword
  let result = await setTimeout(() => {
    console.log("Function: %d executed", input)
  }, 1000 * input);
}

Promise.all([myAsyncFunction(3), myAsyncFunction(2), myAsyncFunction(1)])

When you execute this, you should see that the execution of the above array of async functions is done asynchronously:

[ Node] [nodemon] starting `node dist/app.js`
[ Node] Function: 1 executed
[ Node] Function: 2 executed
[ Node] Function: 3 executed

Conclusion

Hopefully, this tutorial helped to clarify what the async and await keywords are and how they can help you to improve the way you write your TypeScript applications.

If you have any further questions, please let me know in the comments section at the bottom of this article!