DEV Community

SebasQuiroga
SebasQuiroga

Posted on

Async JS Patterns using Promises

So far we have seen that the JS engine is always looking for executing the fastest(easiest) instructions in our code 🤪, leaving aside those lines such as setTimeouts, API calls, renderings that seem slow 😭. Having a solid understanding of the different mechanisms for dealing with this behavior is really useful.

Let’s image that there is a fancy new project in the company we are currently working on 🥳, as a Proof of Concept, the PM tell us to code a program that fetches some files from the Internet, these files can be quite heavy such as books with thousands of pages or pretty light files like recipes with a maximum of 2 pages, as the files API are not still available, the team suggests simulating this delay with setTimeouts 😎, but there is one mandatory requirement, the files have to be fetched in order 😏, the idea behind this is to offer a better user experience, so when a customer decides to download a book and then a recipe, these are downloaded in this particular order and not otherwise.

The first idea we come across with is as follows:

function fetchAPI(rq, time) {
    const apiResponses = {
        'file1': 'First File',
        'file2': 'Second file'
    }

    setTimeout(function () {
        console.info(apiResponses[rq])
    }, time)
}

fetchAPI('file1' , 3000)
fetchAPI('file2' , 100)

// Second file
// First file
Enter fullscreen mode Exit fullscreen mode

The output was not as we want, JS engine is not executing in a sequential ordered manner 😵, as we really want it to 😤.

Intuitively speaking we could have easily inferred that fetchAPI() would first execute the setTimeout, wait until it finishes and then pass to fetch the next file, but it actually didn’t happen.

Betraied

We really need a mechanism for dealing with the crazy way JS engine executes instructions, in the previous posts we studied how callbacks are a pretty initial strategy for this matter, however we ended up with an approach directing us to some inversion of control issues, lack of trust and some reasonability concerns.

Let’s try to think how we as humans are used to solve this problem in our daily lives.

Let’s imagine that after reading this article you write me an email 🤗 and we decide to meet up in person, so we can get in touch 🥳, we both like hamburgers and then we resolve to visit a quite nice place in the city, we go to the counter and order two delicious hamburgers, the lady hands us a ticket with the order 🎫, we sit down and wait, eventually we start smelling the delightful hamburgers and imagining eating it 🍔, finally we are called, we return the ticket and we get two scrumptious hamburgers. We might even not notice yet but the restaurant had a very nice method for serving lots of people concurrently 🤓; we as customers give instructions of how we want a product, they hand us a ticket (pretty much like a promise*), that particular piece of paper means we have the hamburgers, the restaurant will eventually (when the earlier orders are dispatched, like previous tasks) start cooking ours, they can have dozens and even hundreds of orders, but all customers are happy because that ticket is a *virtual** instantaneous hamburger that lets us reason about it.

Ideally there are two possible outcomes: either we all get the hamburgers🥳 or the restaurant run out of them 😱😭. The nice thing here is that whatever happens we are informed (inversion of control reverted) and then we can decide either buying another product or going to another place.

The ticket in the restaurant performs as a promise, a promise is way to represent a future value, as in the analogy, the promise can be somehow resolved or rejected but we are warned of either the case, which gives us again the control.

Let’s try to code once again the same requirement but this time using promises.

function fetchAPI(rq, time) {
    return new Promise((resolve) => {
        const apiResponses = {
            'file1': 'First File',
            'file2': 'Second File'
        }

        setTimeout(function () {
            resolve(apiResponses[rq])
        }, time)
    })
}

fetchAPI('file1', 3000)
.then( resp => {                  // Resolves first promise.
    console.info(resp)
    return fetchAPI('file2', 100) // Return a second promise.
}).then(resp => {                 // Resolves second promise.
    console.info(resp)
})

// First File
// Second File
Enter fullscreen mode Exit fullscreen mode

Fundamentally speaking, we are still using callbacks, but promises behave pretty more awesomely, when we call a promise we give it the control over some business logic (such as fetching data, rendering, etc) similar to callbacks, but the interesting part is that promises give us back the control notifying us if the task could successfully be done or if the task failed, so that we can determine what to do in either the case, in other words, we are not blindly expecting a desired outcome, we can now prepare for no-matter-what the scenario can be.

We have now a more reasonable, reliable, and controllable mechanism for coordinating the nature concurrent way of behavior of JS engine.

Finally I want to write some notes for a correct use of the promises API:

  • For a correct chain of promises,
examplePromise1
    .then(resp1 => {
    ...                                   // Resolves the first promise.
    })
    .then(() => return examplePromise2)   // Returns the second promise.
    .then((resp2) => {
        ...                               // Resolves the second promise.
    })
    .then(() => return examplePromise3)   // Returns the third promise.
    .then((resp3) => {
        ...                               // Resolves the third promise.
    })
Enter fullscreen mode Exit fullscreen mode

The snippet above is just a sketch to show you explicitly the importance of returning each promise so that the resolution of them are sequentially and controlled, otherwise, JS engine will execute those task crazily 🤪 as we don't want.

  • The correct error handling of promise rejections is through the reserved word catch, in a chain of promises one .catch() is enough for any rejection in any of the chain, as the following sketch:
examplePromise1
    .then(resp1 => {
    ...                                    
    })
    .then(() => return examplePromise2)   
    .then((resp2) => {
        ...                               
    })
    .then(() => return examplePromise3)   
    .then((resp3) => {
        ...                     
    })
    .catch(err => {
        throw new Error(err)        // This catch gathers any rejection in any of the three promises
    })
Enter fullscreen mode Exit fullscreen mode

Once read and understood this article I encourage you to study some additional fancy features of the API such us Promise.race, Promise.all, Promise.any reference here.

References

  • [Book] - You Don’t Know JS Async & Performance
  • Promises MDN

Top comments (1)

Collapse
 
ohadbaehr profile image
OhadBaehr
  1. use Promise.all(), what you are doing here is terrible and basicially almost equal using await on every single call. pretty much synchronous.
  2. you are very close to getting into callback hell which is a big no no.

I made this javascript library to tackle the second:
npmjs.com/package/async-unsucked