JavaScript async patterns quick guide

From web applications to servers and mobile apps, from small programs to big projects, JavaScript is everywhere. It's the main choice to embrace any project because, well, it's 2018 and JS is an even more mature language, with an enormous community supporting it.

There's no denying it, JavaScript is currently the top programming language (octoverse.github.com)

In JavaScript, all the code runs synchronously on the event loop, which executes sequentially small chunks of our program. On the event loop each iteration is called a "tick" and runs until the queue is empty. Each chunk has a "tick" to process and after it's completed, the next one starts. For small applications, this is enough, but as we start doing heavier operations that require more time, like accessing a database or fetching data over the Internet, we need better mechanisms to handle them.

Over the years, patterns and libraries emerged in the JS ecosystem to handle asynchronous programming, such as callbacks, promises, generators, async/await, web workers and packages on NPM registry like async, bluebird, co or RxJS. I'll dedicate the following sections to each one and their individual particularities.

Callbacks

In JavaScript, functions are first-class objects and a callback is just a function that is passed as an argument to another function. Also known as high-order functions, the callback should be invoked whenever the asynchronous work is finished.

    fs.readFile('./imaginary.txt', function(err, result) {
        if (err) {
            return console.error('Error:', err);
        }
        return console.log('Result:', result);
    })

Since callbacks are just functions, they are supported by all the environments that run JavaScript, from our browsers to servers that run Node.js. Simple, but yet powerful, this pattern is fundamental in asynchrony. However, it also has its drawbacks.

When projects start to grow and we need to start doing more complex code, it becomes harder to implement generic solutions on our programs, making them harder to read and maintain. When this happen, we start having the pyramid shape of }) similar to what we can see in the following example.

    fs.readFile('./imaginary.txt', function(err, imaginaryResult) {
        if (err) {
            return console.error('Error:', err);
        }
        fs.readFile('./cloud.txt', function(err, cloudResult) {
            if (err) {
                return console.error('Error:', err);
            }
            var res = imaginaryResult + cloudResult;
            fs.writeFile('./imaginarycloud.txt', res, function(err) {
                if (error) {
                    return console.log('Error:', err);
                }
                return console.log('Success!');
            })
        })
    })

This is usually known as "Callback Hell".

However, the worst that we can have with callbacks is the inversion of control. If this happens, we are giving control of the program flow sequence to other parties, making it difficult (or even impossible!) to properly test it.

Promises

Promises are harder to master, but address the inversion of control issue. They are a little slower than callbacks, but in return we get a lot of trustability.

We can always be sure that a Promise will resolve or reject, since they are a “wrapper” around a value that may not exist yet. Promises are a trustable mechanism that also help expressing async code in a more sequential way. They can have, at most, one resolution value, meaning that a Promise always needs to be resolved or rejected.

This is how they solve the inversion of control. Not by removing callbacks, but by creating a mechanism on the wrapper that handles this issue.

Promises provide more functionality, like, for example, the Promise.all() or a Promise.race(). With this kind of operations we can start multiple async requests and proceed only when all of them are resolved or when the first finishes. Also, we can chain multiple Promises on our code without forcing a new level of indentation after each one, using .then().

    var promise = new Promise(function(resolve, reject) {
        fs.readFile('./imaginarycloud.txt', function(err, result) {
            if (err) {
                return reject(err);
            }
            return resolve(result);
        });
    })
    
    promise.then(function(text) {
        console.log(text);
    }).catch(function(err) {
        console.error('Error:', err);
    });

This improves the readability of the code, and the maintainability of the program as well, but not everything is perfect. Since this feature is at framework level, there are multiple implementations that can vary on behavior, plus the overhead cost of time and memory.

Generators

Generators were introduced on ECMAScript 2015 and are functions in which we can use and control the iterator, meaning that functions can be paused and resumed at any time. This is a powerful tool for when we want to get each value only when we need, instead of getting all of them at once. This is possible with the addition of the word “yield” to JavaScript.

    function* iterate(array) {
        for(var value of array) {
            yield value
        }
    }
    
    var it = iterate(['Imaginary', 'Cloud']);
    it.next();
    it.next();
    it.next();
    
    // RESULT:
    // { value: 'Imaginary', done: false }
    // { value: 'Cloud', done: false }
    // { value: undefined, done: true }

We can see in this example that for each next() we receive an object with the value and a flag indicating if the generator functions ended. But generators can be used to control async flows in conjugations with other libraries as well, like in co or redux-saga, of which I will talk more about further ahead.

Async/Await

Finally, ES2017 introduced asynchronous functions making it much more easy to write and read asynchronous code in JavaScript!

They are much more cleaner than the last patterns discussed, and the return of a async function is a Promise! This is very powerful because we have the goodness of both worlds. As we've discussed before, Promises are the safe pick when dealing with complex async operations, but they are not that easy to read and master as async/await code.

One drawback is that it needs a transpilation tool, like Babel, because Async/Await is still syntactic sugar over the promises code.

Since the result is a Promise and can be resolved/rejected, it's important to wrap our await code within a try/catch. This way we are able to properly handle errors on our async code.

    async function() {
        try {
            var result = await fetch('https://imaginaryAPI');
            return result
        } catch (err) {
            return console.error('Error:', err);
        }
    }

Web-workers

Using web-workers, it's possible to run scripts and functions on a different thread. This will not affect the usability of the user interface and has the possibility to send data between workers and the main thread.

The service worker on our browsers is heavily used on progressive web applications. This consists on registering a web worker for our website and deciding which files can be cached or not, and it will make the app usage faster. Also, if the user is offline, some features will still be available. They can also be used to perform heavy operations without freezing the UI or main JS thread.

NPM Libraries

There are several other libraries that try to solve those issues, each using its own techniques. You can find some examples ahead:

Async: this library is good to work with callbacks trying to solve some problems that exist within them, as well as eliminating the callback hell problem! In the last implementations, it's possible to use Async/await code as well.

    async.waterfall([
        function(callback),
        function(callback),
    ], function(err) {
        console.error('Error:', err);
    });

Bluebird: a very performant implementation of Promises that also includes a lot of extra features like cancellation, iteration and Promisify! This last one is a wrapper around functions working with callbacks, returning a Promise for those functions.

    var module = require('imaginary-callback-module');
    Promise.promisifyAll(module)
    
    // RESULT:
    // Now we can call .then() on all module functions, yeaaah!

co: control async flows with generators. This library is a runtime around generators, combining the keyword yield with promises, executing the generator result and returning it as a promise object.

    co(function* () {
        var auth = yield login(username, password);
        return auth;
    }).then(function(result) {
        console.log(result);
    }, function(err) {
        console.error(err);
    });

Redux-saga: A front-end library for React/Redux stack. This is a Redux middleware aiming to make applications side-effects more efficient and easier to manage, as they can be started or cancelled by Redux actions. This implementation makes a heavy use of generators to fetch data over the internet and apply the needed side-effects on our website.

    function* (username, password) {
        try {
            var auth = yield call(login, username, password);
            yield put(someActionToStore(auth));
        } catch (err) {
            console.error('Error:', err);
        }
    }

RxJS: This is a pattern used on Angular apps and it's a reactive pattern. Basically, we create an observable that we can subscribe and wait for changes of which we will be notified. Using this pattern, it's possible to cancel subscriptions and chain observables, for instance.

    Observable.first().subscribe(function(result) {
        console.log('Result:', result);
    }, function(error) {
        console.error('Error:', error);
    })

Conclusion

For simple projects, callbacks are the simplest and easier way to handle with async flows. On bigger projects with a proper setup, I would choose the async/await pattern, as the asynchronicity is easy to read, has a natural error handling and there's no pyramid of death.

This is the kind of syntactic sugar we need on our work, allowing us to write a more readable and maintainable program.

Most popular programming languages in GitHub 2008 - 2018 (octoverse.github.com)

As seen in the above picture, JavaScript continues to be the most used language on github, along with its vibrant community. This is our top pick to handle asynchronous flows, but there are more ways to achieve the same results besides the ones that this guide describes. All in all, it's up to you to choose which is the best for your needs.

At Imaginary Cloud, we simplify complex systems, delivering interfaces that users love. If you’ve enjoyed this article, you will certainly enjoy our newsletter, which may be subscribed below. Take this chance to also check our latest work and, if there is any project that you think we can help with, feel free to reach us. We look forward to hearing from you!