7 minute read

Whenever I am talking to someone about JavaScript, I mostly end up hearing "callback hell" at least once during the conversation. Most of the people are unfortunately staying with the old-school habit, that callbacks are super bad in JavaScript. Let's take a look at how can we remove the need for using callbacks using ECMAScript 6 and TypeScript.

Actually, I didn't know that Callback Hell had its own website!

With the release of ECMAScript 6 (aka ES6 or ES2015) a great amount of new great things have been added to JavaScript (you can see the most important ones here, or take a look at the entire feature list). One of the most important ones for me are Generators (nice explanation can be found on David Walsh's blog). Generally, generators are sort of functions which allow you to pause and resume the control flow.

As mentioned above, generators are used for flow control, which allows for TypeScript (and Babel as well) to allow developers use async/await with functions and transpile the code into generators and promises which are natively supported in ES6 (promises are supported in ES5 as well using a polyfill).

ES6 adoption

One of the most important things to discuss is the adoption of ECMAScript 6 across browsers and runtimes since async/await heavily relies on ES6 features.

It is safe to say, that if you plan to use it within a Node.js application (assuming you have control over the runtime - or at least using some of the newer Node.js versions), may it be a server-side or an Electron based app, you are good to.

However, when speaking of browsers, this may get a little tricky. Currently, as you can see from this compatibility overview, ES6 is supported in all modern browsers (Edge, Chrome, Firefox and Safari), assuming you are running an up-to-date version.

However if you are making an application for users who are conservative or are using an older browser, you may want to pay attention in the next section.

What about ES5?

Recently, TypeScript 2.0 got released, however as much as everyone was expecting it, the async/await didn't make it to the 2.0 release. In the end of the beta announcement post, Microsoft explained that they are not going to ship the support of async/await just yet due to implementation issues. The actual progress can be tracked on GitHub - in the feature request #1664 or on the roadmap.

So as of now, if you are targetting older browsers, you will unfortunately have to stick with Promises and callbacks until TypeScript 2.1 makes it to us, however when writing a modern web application (targeting modern browsers), you shouldn't be afraid of using ES6!

Enough chit-chat, show me!

Alright, alright, let's take a look at some practical sample:

async function Test1(): Promise<string> {
    // Calling Test2 here to show that we can use await within async functions
    let test2 = await Test2();
    console.log("Test1:", test2)
    return new Promise<string>(async (resolve, reject) => {
        setTimeout(function () {
            resolve("Test1 finished");
        }, 1000);
    });
}
async function Test2(): Promise<string> {
    return new Promise<string>(async (resolve, reject) => {
        setTimeout(function () {
            resolve("Test2 finished");
        }, 1000);
    });
}

async function RunTest() {
    console.log("async operation started...");
    let test1 = await Test1();
    console.log("RunTest:", test1);
    console.log("async operation finished...");
}

RunTest();

So in this example, we run RunTest() async function which executes and waits for completion of Test1() and then outputs the result to the console. In Test1() we are calling and waiting for response from Test2() and once we get the response, we proceed forward.

The use of return new Promise and setTimeout with a callback also actually demonstrates how you would wrap functions which only support callbacks instead of Promises.

But let's get back to the original topic, upon "compiling" the code with TypeScript, following code for ES6 is produced:

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator.throw(value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments)).next());
    });
};
function Test1() {
    return __awaiter(this, void 0, void 0, function* () {
        // Calling Test2 here to show that we can use await within async functions
        let test2 = yield Test2();
        console.log("Test1:", test2);
        return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
            setTimeout(function () {
                resolve("Test1 finished");
            }, 1000);
        }));
    });
}
function Test2() {
    return __awaiter(this, void 0, void 0, function* () {
        return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
            setTimeout(function () {
                resolve("Test2 finished");
            }, 1000);
        }));
    });
}
function RunTest() {
    return __awaiter(this, void 0, void 0, function* () {
        console.log("async operation started...");
        let test1 = yield Test1();
        console.log("Test1", test1);
        console.log("async operation finished...");
    });
}
RunTest();
//# sourceMappingURL=HelloWorld.js.map

So as you can see, TypeScript produces an __awaiter function and generators for each of the async functions, also using the yield keyword which altogether mimic the async/await operation.

Using with existing libraries

import * as sqlite3 from "sqlite3";

async functin Connect(): Promise < sqlite3.Database > {
    return await new sqlite3.Database("db.sqlite");
}
async function Run() {
    let database = await Connect();
}

This example shows how easily you can use TypeScript and an existing library together with async/await flow as opposite to returning and resolving an actual promise. It actually means that you don't have to wrap the library functions into async functions, but they are going to work out of the box!

Wrap up

With the couple of projects which are written in JavaScript (mostly server-side code) we are migrating some of the code into TypeScript for better maintainability and as a part of this, we are making use of the async/await flow there.

Generally, it made our code cleaner and more easy to understand for other developers as well.

To submit comments, go to GitHub Discussions.