Roger Ngo's Website

Personal thoughts about life and tech written down.

A Quick Introduction to JavaScript Promises

Introduction and Motivation

When asynchronous programming is introduced to any codebase, the complexity of development suddenly becomes much higher. Asynchronous operations introduces things such as race conditions, nested callbacks and out of order execution -- all prone to introducing bugs.

Not too long ago, the pattern to perform any asynchronous operation in JavaScript was to first write the asynchronous code, then write a callback function to handle any returned result, or error.

AJAX, which follows the fundamental pattern of making an asynchronous HTTP request and then processing the response from the server using a callback function is a common situation where one has to consider asynchrony in JavaScript development. In the era of Web 2.0 and in this present day, async JavaScript has become more and more important in delivering a rich user experience for web and mobile applications.

The common mistake in asynchrony is to expect data to be available right when the async operation is invoked. For those with experience in this, the solution is to provide a callback function to actually process this result.

Complication occurs when another asynchronous operation must be made in order to further process the data initially retrieved. To do this properly, one could then perform the async operation with the callback nested under the current callback, creating a pattern similar to this:

request.get(url, (error, response, body) => {
    // process the result
    request.get(url2, (error, response, body) => {
        // process the result returned by this second async operation.
    });
});
        

Over time, with the increasing use of async operations performed over the wire to deliver data, the management of callback functions within asynchronous invocations can lead to code that is very hard to manage. Nested callbacks cause confusion and this has been termed "callback-hell".

Take a simple HTTP request for example, where we fetch Reddit's AMD subreddit and filter the results for URLs that lead to meta-posts:

const fetchAmdSubreddit = () => {
    request.get('https://reddit.com/r/amd/.json', (error, response, body) => {
        if(error || response.statusCode !== 200) {
            console.log("An error occurred.");
        }
        const result = JSON.parse(body);

        let urls = [];
        result.data.children.forEach((listing) => {
            if(listing.data.domain === 'self.Amd') {
                console.log(listing.data.url);
                urls.push(listing.data.url);
            }
        });

        return urls;
    });
};

The function fetchAmdSubreddit is simple. It makes the request to the URL https{//reddit.com/r/amd.json, obtains the JSON returned by this operation. Our callback function will then filter out the data for the appropriate URLs before returning the result. This all happens asynchronously using HTTP requests.

The result returned is something similar to this:

https://www.reddit.com/r/Amd/comments/7jsbzh/hope_youre_all_enjoying_adrenalin_so_far_lets/
https://www.reddit.com/r/Amd/comments/7moyrn/i_thought_ryzen_mobile_apus_were_supposed_to_be/
https://www.reddit.com/r/Amd/comments/7mo0x1/i_just_bought_the_envy_x360/
https://www.reddit.com/r/Amd/comments/7mndye/when_in_general_is_agesa_1072_coming_to/
https://www.reddit.com/r/Amd/comments/7mmu3y/ram_for_ryzenam4_with_the_best_timings/
https://www.reddit.com/r/Amd/comments/7mn91i/im_planning_to_upgrade_my_cpu_cooler_from_amd/
https://www.reddit.com/r/Amd/comments/7mnoea/what_are_your_thoughts_about_apus/
https://www.reddit.com/r/Amd/comments/7mmjcf/why_do_the_new_metrics_show_up_as_an_option_for/
https://www.reddit.com/r/Amd/comments/7mocay/rx480_8gb_vs_rx580_8gb/
https://www.reddit.com/r/Amd/comments/7mpt2b/ryzen_owners_whats_your_minimum_mhz_increase_to/
https://www.reddit.com/r/Amd/comments/7mmvkt/disabling_smt_on_r5_1600/

Now, suppose we want to further process these URLs and retrieve the main post content for each of URL. This would require another GET request made to the Reddit API, and thus another asynchronous operation.

In order to guarantee that we have the resulting list of URLs before we make the subsequent individual requests, we must process them under the initial request callback:

const fetchAmdSubredditGetAllComments = () => {
    request.get('https://reddit.com/r/amd/.json', (error, response, body) => {
        if(error || response.statusCode !== 200) {
            console.log("An error occurred.");
        }
        const result = JSON.parse(body);

        let urls = [];
        result.data.children.forEach((listing) => {
            if(listing.data.domain === 'self.Amd') {
                console.log(listing.data.url);
                urls.push(listing.data.url);
            }
        });

        urls.forEach((url) => {
            request.get(url + '/.json', (error, response, body) => {
                if(error || response.statusCode !== 200) {
                    console.log("An Error Occurred grabbing", url);
                }

                let result = JSON.parse(body);

                console.log(result[0].data.children[0].data.selftext);
            });
        });
    });
};

Similar to the first example, we first make our asynchronous HTTP request to fetch the URLs in the particular subreddit. Within the outer callback function, we process the results to filter out the URLs which relate to meta-subreddit posts. When we have the required list of URLs, we perform another request for each URL in a loop — fetching all content. We have an inner-callback function within this request to output the content of the post.

I just bought my sapphire nitro+ vega 64 and it’s crammed up with my nitro+ rx 580 ...
... for visibility as we continue to reference this thread down the road.

It is quite easy to see that after some time, code like this can get very confusing. Is there a way to manage all this better?

Yes, JavaScript has the Promise object made available to us in allowing better management of asynchronous code. Promise objects take code that is asynchronous and allows it to managed better by preventing numerous nested callbacks. Let’s dive further in what this means.

Promises are objects that will be placeholders for a result computed through some sort of asynchronous operation. The Promise constructor takes a single function argument which in turn takes 2 references as arguments: resolve, and reject.

Within this function, the asynchronous operation is performed immediately. The outcome of this asynchronous operation then determines which state the Promise will be set to.

An essential concept to understand is that the JavaScript Promise object and its API is not a magical tool that will suddenly make asynchronous, synchronous. However, an argument could be made in that it does make asynchronous code read as if it was synchronous. This leads to better interpretation of control flow for the developer. The pattern is usually the following:

Invoke a promise with two callback functions: resolve and reject. The resolver callback is the function that runs when the promise is fulfilled, or rejected.

In summary:

  1. Invoke the asynchronous operation
  2. Asynchronous operation has completed
    • If an error -> call reject. The promise is rejected.
    • If success -> call resolve. The promise is fulfilled.

If the async operation succeed the promise will then be fulfilled by calling the provided resolve function. Otherwise if an error happens, the promise will then be rejected and the reject callback will be called.

Depending on what the state of the Promise is, invocation of functions provided in the then(), or catch() functions will occur.

If the promise is then fulfilled, a function provided in then call will be executed. Otherwise, if an exception or rejection had occurred, the function provided in the catch call will be executed.

This leaves us with several states in which a Promise object can be:

  1. Pending - The asynchronous operation hasn't been invoked yet.
  2. Fulfilled - The asynchronous operation succeeded and the resolve function is called.
  3. Rejected - The asynchronous operation encountered an error and the reject function is called.

Example with the Promise Object

Suppose we simulate an asynchronous operation with the following code:

const performAsyncOperation = (callback) => {
console.log("Beginning async operation.");
let x = 0;

setTimeout(() => {
        console.log("Async operation returns.");
        x = 1;

        callback(x);
    }, 5000);
};

The asynchronous operation essentially sets the value of x, which is initially 0, to 1 after 5 seconds. The callback function with the value of x is passed in as a parameter to be handled.

Here is an example how we can leverage the Promise object to call our asynchronous operation:

// We will now provide the appropriate functions to run once the async operation has completed
let myPromise = new Promise((resolve, reject) => {
    performAsyncOperation((x) => {
        if(x === 1) {
            resolve(x);
        }
        else {
            reject(x);
        }
    });
});
    

A good way to think about this is that myPromise is a Promise object. This promise object serves as a template on how to handle a specific asynchronous operation. The promise object injects two callback functions which are passed by the caller: resolve, and reject. As stated before, resolve is the callback function to be invoked if the async operation succeeds, and reject is the callback function to be invoked if the async operation fails.

The promise object wraps itself around the asynchronous operation. With this, we can simply invoke the then/catch functions to begin the asynchronous operation. The parameter of the then function that is run when the Promise resolves, while the parameter of the catch function is invoked when the promise is rejected. An example of usage would be like this:

console.log("Async operation is going to be called now.");

myPromise.then((x) => {
    console.log("Async operation was successful with the value of x being", x);
}).catch((x) => {
    console.log("Async operation resulted in an error with value of x being", x);
});

console.log("We have called the async operation.");

Our output after invoking the piece of code will be:

Beginning async operation.
Async operation is going to be called now.
We have called the async operation.
Async operation returns.
Async operation was successful with the value of x being 1

Given this output, we notice a few things here:

  1. When the async operation is called, the operation is non-blocking and we immediately execute code following the promise invocation — which is a log message indicating we have called the asynchronous operation.
  2. It is then after some time, about 5 seconds, in which the asynchronous operation actually invokes our callback function with the appropriate log messages.
  3. Our function passed as the resolver for the called promise object was called as seen by the output.

Each chain from a then/catch call will return a new Promise object. This allows us to actually chain multiple then/catch calls together. When these promises are chained together, each call will return a new promise object, which will then allow us to process the result further with a new call that is guaranteed to be invoked after the initial fulfillment of the promise.

Building on our example above, suppose we want to do something with the value returned by our first asynchronous call. Let's just increment the value of x returned. To do so, we would simply need to just return the value handled by the first resolve function.

By returning the value, the value will be passed to the next callback function returned by the new promise object, and can be manipulated:

console.log("Async operation is going to be called now.");

myPromise.then((x) => {
    console.log("Async operation was successful with the value of x being", x);
    return x;
}).catch((x) => {
    console.log("Async operation resulted in an error with value of x being", x);
}).then((prevX) => {
    console.log("I won't execute until the first asynchronous operation has been completed.");
    let x = prevX + 1;
    console.log("x is now", x);
});

console.log("We have called the async operation. -- but my code comes after the async call.");

The output will now be:

Beginning async operation.
Async operation is going to be called now.
We have called the async operation.
Async operation returns.
Async operation was successful with the value of x being 1
x is now 2

Chaining these promises allows us to successfully process multiple asynchronous operations together to produce reliable results.

Again, I will stress once more — Promises do not turn asynchronous code into blocking, synchronous code. Code will continue to execute as any other asynchronous code and will only resolve, or reject when the async operation has been completed.

Therefore, all subsequent code that follows the async calls that happens to be synchronous will continue to execute. This means that it is not guaranteed (as always) that the result of the asynchronous operation will be available by the time it is used outside of its chain.

Real World Example

Now, let's take our previous Reddit API call example and transform that code into using Promise objects.

First, create a promise object that will make the request to retrieve the URLs:

const redditPromise = new Promise((resolve, reject) => {
    request.get('https://reddit.com/r/amd/.json', (error, response, body) => {
        if(error || response.statusCode !== 200) {
            reject(error || response.statusCode);
        }
        resolve(body);
    });
});

We can see that this Promise object is relatively light-weight. The object simply performs the HTTP GET request to the URL provided. After doing so, the callback function for this request will then invoke the resolve and reject functions depending if an error had occurred during the processing of the request on the server's end.

To invoke the redditPromise, we will now call then/catch and provide the proper callback functions to handle the promise resolution/rejection.

Since the code is for educational purposes, there will be some hardcoding here.

redditPromise.then((response) => {
    let result = JSON.parse(response);

    let urls = [];
    result.data.children.forEach((listing) => {
        if(listing.data.domain === 'self.Amd') {
            console.log(listing.data.url);
            urls.push(listing.data.url);
        }
    });

    return urls;
}).catch((error) => {
    console.log("error!", error);
}).then(function(urls) {
    urls.forEach((url) => {
        request.get(url + '/.json',(error, response, body) => {
            if(error || response.statusCode !== 200) {
                console.log("An Error Occurred grabbing", url);
            }

            let result = JSON.parse(body);

            console.log(result[0].data.children[0].data.selftext);
        });
    });
}).catch(function(error) {
    console.log(error);
});

Running this, we will see that we get the exact same result — except that now the code is a bit more manageable in the callback-sense.

This code can be improved further. We can see that with our second asynchronous operation, we are executing multiple asynchronous operations to fetch our post content for each URL. We can actually create multiple promises and have them all be resolved in one shot. How would we go about doing this? A little refactoring, of course! ;)

Our redditPromise object stays the same, but we notice that given each URL returned by the HTTP request performed by the redditPromise object, we perform many HTTP requests for each URL. We can then refactor the second async operation into its own promise object like so:

const redditPostContentPromise = (url) => {
    return new Promise((resolve, reject) => {
        request.get(url + '/.json', (error, response, body) => {
            if(error || response.statusCode !== 200) {
                reject(error || response.statusCode);
            }

            resolve(body);
        });
    });
};

Here, we have cleverly created a new function to take a URL as a parameter and return a new Promise object that will invoke an HTTP request asynchronously using that URL.

Now, let's re-work our chain of Promise calls to leverage our new function that will return the Promise object depending on the URL given:

redditPromise.then((response) => {
    let result = JSON.parse(response);

    let promises = [];
    result.data.children.forEach((listing) => {
        if(listing.data.domain === 'self.Amd') {
            console.log(listing.data.url);

            let url = listing.data.url;
            promises.push(redditPostContentPromise(url));
        }
    });

    return Promise.all(promises);
}).catch((error) => {
    console.log("error!", error);
}).then((result) => {
    result.forEach((b) => {
        let result = JSON.parse(b);

        console.log(result[0].data.children[0].data.selftext);
    });
}).catch((error) => {
    console.log("error!", error);
});

Just like before, we invoke the first Promise object, redditPromise to get the list of URLs. Instead of pushing each URL to an array for processing, we have defined a new array called promises that will store an individual Promise object for each URL we iterate over in our first resolve function.

Once that has completed, we will then attempt to resolve all these Promises by invoking the Promise.all() function. It will just take in the array of promises. By returning the result, we can chain another call to handle the aggregated result that is returned by the Promise.all() call.

Much cleaner.

Here is the code altogether:

const request = require('request');

const redditPromise = new Promise((resolve, reject) => {
    request.get('https://reddit.com/r/amd/.json', (error, response, body) => {
        if(error || response.statusCode !== 200) {
            reject(error || response.statusCode);
        }
        resolve(body);
    });
});

const redditPostContentPromise = (url) => {
    return new Promise((resolve, reject) => {
        request.get(url + '/.json', (error, response, body) => {
            if(error || response.statusCode !== 200) {
                reject(error || response.statusCode);
            }

            resolve(body);
        });
    });
};

redditPromise.then((response) => {
    let result = JSON.parse(response);

    let promises = [];
    result.data.children.forEach((listing) => {
        if(listing.data.domain === 'self.Amd') {
            console.log(listing.data.url);

            let url = listing.data.url;
            promises.push(redditPostContentPromise(url));
        }
    });

    return Promise.all(promises);
}).catch((error) => {
    console.log("error!", error);
}).then((result) => {
    result.forEach((b) => {
        let result = JSON.parse(b);

        console.log(result[0].data.children[0].data.selftext);
    });
}).catch((error) => {
    console.log("error!", error);
});

Although after all the refactoring we had done in comparison to the first non-Promise based async code, the functionality is the same. Both code snippets will give us the exact same results in the exact same manner. It is all about the pattern!

async/await

Promises are not immune to callback hell. Over time, Promises can produce the same situation it was created to avoid in the first place. In order to make structuring of asynchronous operations in JavaScript a little easier to understand, we can then leverage some syntactic sugar by using async and await.

A function defined with the async keyword can use the await keyword in its body. This turns the asynchronous function into a function that can be in effect, read synchronously. To invoke each asynchronous operation, the await keyword preceding a Promise object tells us that the operation is "yielded" before the next line can begin execution.

Keep in mind that this does not make asynchronous code, synchronous. It merely allows us to structure our code so we can read the asynchronous operation as synchronous.

async/await therefore, are syntactic sugar placed on top of the standard Promise state handling. You still need Promise objects to use async and await, so it is not correct to assume that async and await are separate language features in themselves.

Conclusion

I won't go in depth to the API for the Promise object here, but it is good to know what is available for use. I want to simply be able to help convey the pattern of promises in this discussion and how they can be used to manage asynchronous code better.

There is so much more to producing great control flow when programming asynchronous operations using Promise objects. I am still learning myself and each situation in which I find myself using Promises gives me a better approach into structuring my code.