In many places in our codebase, I see we use a lot of callbacks. This is common in a lot of places, especially coming from the jQuery world, for example:

callSomeFunction(12345, (result) => { ... });

By structuring a function this way, you will often hear some people refer to this as causing ‘callback hell’, because each function you want to call will nest..

callOneFunction(arg1, (result1) => {
    callSecondFunction(arg2, 12345, (result2) => {
        callThirdFunction("banana", (result3) => {
            // Will it ever end?
        }, () => {
            // Oh man, an error too?
        });
    });
});

A few issues can arise from this:

  1. You might easily forget how many arguments a function has, which one’s the callback, as you’re mixing incoming and outgoing data
  2. What if you forget the error handler?
  3. What if there’s an exception?

Enter.. The Promise – a standard for encapsulating asynchronous requests, by providing an API for chaining the results once they arrive. It’s very similar to the above, but is standardized.

function yourPromiseFunction(arg) {
    return new Promise((resolve, reject) => {
        // Perform some actions and resolve..
        resolve(arg * 2);
    })
}

Now don’t get be wrong, it looks a lot like the previous callback way of doing things; however, we’ve now encapsulated the results in a standardized way.. which you can ALWAYS call then on. You can also call catch and finally on it, in case of errors. And that’s not all! You can wait for multiple parallel Promises to resolve before continuing.

Say you wanted to wait for two API requests to complete and come back before proceeding..

function callRandomApi() {
    return new Promise((accept, reject) => {
        // .. do the work then accept(response)
    });
}

const allTheApiCalls = Promise.all([
    callRandomApi(),
    callRandomApi(),
    callRandomApi(),
    callRandomApi(),
]);

allTheApiCalls.then(([r1, r2, r3, r4]) => {
    console.log("API Results:", r1, r2, r3, r4);
});

And finally, there’s one more thing that they’ve recently added to JavaScript which removes the complications of nesting and callback hell a bit: async/await.

Make sure you understand the above examples, because this one gets a little tricky. Async/await is just a fancy new way of wrapping and unwrapping things in Promises for you, so that it becomes a little more seamless for you. There are also some superpowers it lets you do that you would otherwise have had to write some insane logic for in the past. But essentially it lets you:

// Go from this..
function yourNormalFunction() {
    firstCall(12345).then((result) => {
        secondCall(result * 2).then((nextResult) => {
            finalCall(nextResult);
        }, handleError);
    }, handleError);
}

// To this..
async function yourAsyncFunction() {
    try {
        const result = await firstCall(12345);
        const nextResult = await secondCall(result * 2);
        finalCall(nextResult);
    } catch (e) {
        handleError(e);
    }
}

… but inside, it’s all just Promises.

I hope this helps explain something that I’ve started to love about JavaScript’s new features. Let me know if you have any questions or want a deeper explanation and I’ll be glad to walk through it!

For more reference, check out: Mozilla Developer - Promises