DEV Community

Cover image for JavaScript Promises: All you need to know
cu_0xff πŸ‡ͺπŸ‡Ί for DeepCode.AI

Posted on • Originally published at Medium

JavaScript Promises: All you need to know

Without any doubt: The most frequent suggestion we see corrected in the wild is actually a pretty trivial one (who would have thought). We found roughly 20,000 changes in our training repos addressing one thing: Unhandled rejections in promises. Maybe it is time to provide a fundamental guide.

If you want your code to be scanned, just go to deepcode.ai.

Why are promises needed?

JavaScript provides a single threaded environment: No two pieces of code run at the same time. This reduces issues regarding consistency with mutexes (think race conditions), but add the need for others. JavaScript makes use of callback functions to provide asynchronous calculations. While this in itself is a possible way to write asynchronous code, it leads to what is known as Pyramid of Doom where you have callback in callback in callback in ... until you totally lose track what happens when. Here come promises to help.

A promise is an object that represents eventual completion or failure of an asynchronous operation and its subsequent result value.

Dive deeper with MDN docs on Promise or Javascript.info

Basic Functionality

let promise = new Promise(function(resolve, reject) {
    // Here is the workload

    if( /* Success */) {
        /* result can be a Promise or thenable */
        resolve(result)
    }
    else {
        /* Best practice: reason is instanceof Error */
        reject(reason)
    }
})

Within a promise, two callback functions are provided: resolve() shall be called in the case of a positive outcome. The result can be handed on as a parameter. reject shall be called in the case of an error (including an explanation as a parameter). There is no third option (like cancelled or whatever). A promise is always in one of three states:

  • pending: Initial state and still work in progress
  • fulfilled: Completed successfully
  • rejected: Operation failed

Usage

Promises can be hand-on within the app as long as their value is not directly needed. This gives the system the opportunity to resolve whatever is asked in the background without the need to wait for things to settle. When the application has the need for the result to proceed, it can query the result and react upon it.

Side note: Try to delay the need for a result of a promise as much as possible to gain the maximum benefit of using promises.

Example:

const promise1 = new Promise((resolve, reject) => {
    resolve('Here is the result');
});

promise1.then(/* Success */(value) => {
    // Do something with the result
    },
    /* Fail */ (error) => {
    // React on error case
    }
);

If the success handler is not provided (as a function), it is replaced by the identity function (aka, it returns the parameter).

This is the lifecycle as provided by Mozilla's documentation (yet omitting finally()):

Promises Lifecycle

As you can see in the life cycle: There are two options to provide an error handler: Either by providing a callback as a parameter for then or by explicitly providing a callback in catch.

async and await

async in front of a function means the function always returns a promise. If the function returns a different type, it is wrapped in a promise.

async function f() {
    return 42; // We will get a promise with result 42 and success state
}

f().then(console.log) //prints 42

So far, we only stacked on promises but what if we really need the value to be settled. Here comes await. This keyword makes JavaScript wait until the promise is settled. Obviously, while the system stops here and waits with the execution of the code until the promise is settled, it continues to execute other code. We will see more sophisticated methods to combine promises a little later.


async function f() {
    let promise = new Promise((resolve, reject) => {
        // Do something here
        resolve(result);
    });

    let result = await promise;
}

f();

Note: await only works inside an async function. You cannot use await on top level code but you can wrap it in an anonymous async function:

(async () => {
    let response = await promise1;
})();

Chaining of Promises

Nice in-depth article on the topic here
The result of a promise can be piped through subsequent functions. Those functions can provide alternative parameters to their subsequent followers allowing to build a pipeline to manipulate data such as filtering or enriching. The then function returns a promise itself.

new Promise((resolve, reject) => {
    // Promise
    resolve(result);
}).then((result) => {
    // First handler
    return handler_result;
}).then((handlerResult) => {
    // Second handler
    return secondHandlerResult;
}).then((secondHandlerResult) => ...

Objects providing a then function, are called thenable. These objects can be used with then or await as we saw above.

class Thenable {
    then(resolve,reject) {
        if(success) //in case of success 
            resolve(result);
        else
            reject(error);
    }
};

async function f() {
    let result = await new Thenable();
    }

f();

There are two more important functions:

const promise1 = new Promise((resolve, reject) => {
    throw 'Error'; // calling reject() also leads to rejected state
    })

promise.catch((error) => {
    // Handle your error here
    }).finally(() => {
    //Do clean up needed whether success or failure
    });
  • catch: This is actually a shortcut for then(undefined, onRejected) and provides a promise that handles error cases. It does an implicit try-catch (as shown above).
  • finally: Again a promise which is called in both end states. Helps to reduce code doubling in then and catch as you can put all clean up needed in both cases in here.

Side note: Since both catch() and finally() return promises, you can chain one them again by using then.

If there is no error handler provided ("unhandled rejection"), the error bubbles up and leads to the script crashing with an error message. This is what DeepCode complains when it finds an unhandled rejection.

Combination of Promises

Promises can be collected and combined in various ways. In general, combining promises in a iterable object and provide this to the combination.

Promise.all([promise1, promise2, promise3, promise4]).then((values) => {
    console.log(values); // Result Array [result1, result2, result3, result4]
    }).catch((error) => {
    // something went wrong
    })

Here is a table of all combination functions:

Function Explanation Typical Use Reaction on Rejection
Promise.all() Returns a single promise to collect all results of the input promises Multiple asynchronous tasks that are dependent on each other Will reject immediately on one input promise reject
Promise.allSettled() Returns a single promise to collect all results of the input promises Multiple asynchronous tasks that are not dependent on each other Will collect all results and rejects
Promise.race() Returns a single Promise that returns as soon as one of the input promises resolve or rejects Used in batching or for time-outs Immediately returns on one input rejects
Promise.any()(still experimental) Returns a single Promise which resolves as soon as one of the input promises resolves but wait for all promises to reject before reject When two resources race to provide data (e.g., cache versus network) Swallow rejections until all input promises reject

Next Steps

From our point of view, knowing the above should equip you to understand and use promises like a pro. Did we miss something, let us know. And, make sure to check your code on deepcode.ai.

Latest comments (0)