Shubho.dev logo
JavaScript

How I use JavaScript Promises

Async programming in JavaScript was scary for me. The only async paradigm I was comfortable with was jQuery's $.ajax. However, I went full vanilla JavaScript for the past 8 years, and when I started working with NodeJS, I had to learn Promises. I haven't dabbled much with third-party libraries like Bluebird. I have the most experience with native Promise.

My main issue with Promise (or asynchronous paradigm in general) used to be when I wanted to execute statements after the Promise statement began. It took some time to realise that once a Promise statement fires, there is no way to cancel it. Another issue was Promise chaining. This one was a kicker. My earlier functions with Promises always looked like friends of callback hell. After all these years and working on a couple of big projects, I can safely say I love Promises. Even though async/await is the new fad, I still love Promises.

So here is how I use Promises to make my coding life simpler.

Create a Promise skeleton

Whenever I create a new function that returns a Promise, I create the skeleton first. Since the function cannot return anything other than Promise based values, I always wrap all the statements of the function within the Promise.

js
function sample() {
    return new Promise(function(resolve, reject) {
        // The function body
    });
}

The above sample function wraps its entire statement within the Promise, returning immediately. You can either resolve() or reject() the output you want from the body. This way, I never make a mistake of not returning a Promise. It also helps me in creating Promise chains. Whenever in a chain, I realise I need a new function, I create the skeleton name it appropriately and finish the main chain. Then I come back one by one and finish the individual functions.

Promise chains - Points to remember

Promise chaining is tricky. If we are not careful, we can have a new type of callback hell. An example:

js
function promiseCallback() {
    return new Promise((resolve, reject) => {
        aNewFunction()
            .then((values) => {
                someOtherFunction(values)
                    .then((someOtherValue) => {
                        // Do something
                        resolve(someOtherValue);
                    })
                    .catch((err1) => {
                        // Error in inner function
                        reject(err1);
                    });
            })
            .catch((err) => {
                // Error in outer function
                reject(err);
            });
    });
}

In the above sample aFunction() and someOtherFunction() are two functions returning Promises. If you see carefully, the sequence looks like a callback hell. The inner then and catch the chain, and outer ones are independent. We cannot handle errors in a common catch block, and we need to be careful that the inner functions are always the last line within their outer then() otherwise we can't control the execution flow.

A better way with chains:

js
function promiseCallback() {
    return new Promise((resolve, reject) => {
        aNewFunction()
            .then((values) => {
                return someOtherFunction(values);
            })
            .then((someOtherValue) => {
                // Do something
                resolve(someOtherValue);
            })
            .catch((err) => {
                // Error in outer function
                reject(err);
            });
    });
}

Returns within the then chain can only have three types:

  1. Promise - A then function in a chain can return a Promise. It's result is passed to the next then.
  2. Scalar Value - A then function in a chain can return a value like a String or a Number. This value is passed to the next then as is and the chain can continue.
  3. Throw - A then function can throw an error, which moves the execution to the catch block.

As long as all your returns within a then follow the above three types, you shouldn't have issues following your Promise chain.

Note
Remember to always resolve() or reject() in the last then or catch of the chain.

When to create a new Promise function

Within a Promise chain, if there are multiple if-else conditions, and each condition can lead to different Promise results, it is an excellent time to create a new function that returns a Promise. This way, the Promise chain returns a single statement calling the new function.

Handling a scalar value or a Promise function in one step

Assume we have a function which gets the marks attained by a student using his roll number. However, the function either takes a roll number as an input or the name of the student. The marks can be attained from the DB only using the roll number. Here is some pseudo-code.

js
function getMarks(obj) {
    let rollNumberPromise = null;
    if ('rollNumber' in obj) {
        rollNumberPromise = Promise.resolve(obj.rollNumber);
    } else if ('studentName' in obj) {
        rollNumberPromise = getRollNumberFromName(obj.studentName);
    }

    if (!rollNumberPromise) {
        reject('Nothing worked');
    }

    rollNumberPromise
        .then((rollNumber) => {
            return get_marks_from_db(rollNumber);
        })
        .then((marks) => {
            resolve(marks);
        })
        .catch((err) => {
            reject(err);
        });
}

function getRollNumberFromName(studentName) {
    return new Promise(function(resolve, reject) {
        fn_to_get_roll_number_from_db(studentName)
            .then((rollNumber) => {
                resolve(rollNumber);
            })
            .catch((err) => {
                reject(err);
            });
    });
}

function fn_to_get_roll_number_from_db(studentName) {
    return new Promise(function(resolve, reject) {
        // some code
    });
}

function get_marks_from_db(rollNumber) {
    return new Promise(function(resolve, reject) {
        // some code
    });
}

getMarks(obj) takes an Object as an input. We create a local variable rollNumberPromise. If the rollNumber is already present, we save the value in the variable using Promise.resolve(). This creates a Promise which resolves when called with the value. If student’s name is sent, then we save the call to the function getRollNumberFromName(studentName) to the local variable. Calling rollNumberPromise.then() returns a rollNumber whether it is received from the DB or sent directly as input to the function. Using it this way ensures that getMarks() has a single Promise chain, rather than an if-else condition based on whether the input passed was a number or a name.

Invoke a Promise at the end

As mentioned before, once a Promise, once invoked, cannot be cancelled. Any statements which do not depend on the Promise output and which can be carried out independently without an async call should complete before you start a Promise chain in your function. Once a Promise chain begins, any subsequent steps must be within the then chain. The only exception to this is when you do not care of the Promise value, and you want the Promise to execute in the background while your primary function keeps running.

Conclusion

Promises are difficult. However, with practice and following some rules, it makes working with them a charm. I strictly follow the above rules, and I never go wrong with Promises these days. Find out what you are comfortable with and create your own rules.

Featured Image courtesy Pexels at Pixabay