Introduction to Promises

Introduction to Promises

Promises have been a great addition to JavaScript, and have provided great building blocks for the new async/await syntax as well as providing a great user experience when dealing with asynchronous code.

When promises are used with await they give the developer the ability to write code in a way that can be compared to synchronous versions of the same concepts.

In JavaScript, a Promise is a representation of a value that will be available at some point, it can be compared to ordering takeout over the phone and waiting for it to be delivered, then the order is fulfilled. With your fresh food ready to consume, sometimes though you might catch a call that there is something wrong with the payment so the order couldn't be fulfilled, but finally you figure something out.

Promises provide a few functions, but provides a great amount of power and flexibility. In this article we will discuss a few patterns that we can use throughout our code, and understand how promises work a bit better.

What are promises & how to consume them

There have been a few variations of promises over time, a good starting point to understand exactly how promises work is to read over the Promises/A+ standard, this won't be required for this article, so don't worry if that page looks a bit daunting!

From a promise consumer's point of view - a promise is an object with three functions, then, catch, and finally (depending if the environment supports the function).

Each function takes a function as the first argument, the then function additionally accepts a second argument which is a shorthand for invoking catch.

I must also mention at this point that Promise.resolve(value) will result in a fulfilled promise, and Promise.reject(value) will result in a rejected promise.

By running the below code, utilising the then function, we will see the value 42 be logged in our console:

js
const myValue = 42; const promise = Promise.resolve(myValue); // Will log 42 promise.then(function onFulfilled(value) { console.log(value) });

By running the below code, utilising the then and catch functions we will see the error Something went wrong! be logged in our console twice:

js
const myError = new Error("Something went wrong!"); const promise = Promise.reject(myError); // Will log the error twice promise.then(undefined, function onRejected(error) { console.error(error) }); promise.catch(function onRejected(error) { console.error(error) });

In the above example, when you invoke the catch function, internally then is invoked with the first argument as undefined. So functionally both methods are the same, yet it is nicer to know your intention within your code without needing to question what's happening. So try to stick to catch if you aren't also defining a onFulfilled function as the first argument with then.

If you want a function to be invoked whether the promise was fulfilled or rejected, you can utilise the finally function, this code will log 42 then Finally!:

js
const myValue = 42; const promise = Promise.resolve(myValue); promise // Will log 42 .then(function onFulfilled(value) { console.log(value) }) .finally(function onRejected() { console.log("Finally!") });

Something to note here is that finally isn't passed any value. This is because the promise could either be fulfilled or rejected by the time finally is invoked. It should also be known that finally doesn't change the value of the returned promise (explained shortly).

In the above example we chained our functions. This is because the return value of then, catch, or finally is a promise. In the case of then and catch the returned promise will have the value of the returned value from said functions. In the next example, 42 will be logged, followed by 43:

js
const myValue = 42; const promise = Promise.resolve(myValue); function logAndAdd(value) { console.log(value); return value + 1; } promise // Will log 42 .then(logAndAdd) // Will log 43, and return a promise with the value 44 .then(logAndAdd)

This works great because it gives us a way to transform the result of the promise. For example, we want to try and find a value from our database, and if it doesn't exist we might want to create it:

js
import { getItem, createItem } from "./store"; const itemId = 42; getItem(itemId) .catch(function onRejected(error) { if (error.message !== "Not Found") { throw error; } return createItem(itemId); }) .then(function onFulfilled(item) { // Do something with our item });

You may have noticed something special with our catch function above, we re-throw the error! We can do this because any errors that are thrown within our "handler" will result in a rejected promise with the value of the error thrown. This can be visualised by imagining the promise is calling our handler like this:

js
function invokeHandler(handler, value) { try { const handlerResult = handler.call(undefined, value); return Promise.resolve(handlerResult); } catch(error) { return Promise.reject(error); } }

Calling this function will always result in a promise, but if the handler ever was to throw an error, we would catch said error and create a new rejected promise. This same code can be used for both then and catch.

The finally handler is a bit different. The value returned is ignored, and the original promise value is used. Yet any error thrown in finally still results in a rejected promise. This can be visualised like so:

js
function invokeFinallyHandler(handler, value) { try { const handlerResult = handler.call(undefined); return Promise.resolve(handlerResult) .then(function onFulfilled() { return value }); } catch(error) { return Promise.reject(error); } }

Notice how we still want to resolve the handlerResult. This is because handlerResult could be any value, including a Promise. When the handlerResult value is a promise, the returned promise will "follow" the result of said value.

This can be explained by thinking of it as functionally the same as the first example:

js
const myValue = Promise.resolve(42); const promise = Promise.resolve(myValue); // Same as Promise.resolve(42) in this case // Will log 42 promise.then(function onFulfilled(value) { console.log(value) });

Something else to note when chaining promises is that if any handler in that chain returns a rejected promise, then any then handler will be ignored until the rejection has been handled:

js
Promise.resolve() .then(() => { throw new Error("Ouch!") }) .then(function onFulfilled() { console.log("A!") }) // Won't be invoked .then(function onFulfilled() { console.log("B!") }) // Won't be invoked .catch(function onRejected(error) { console.log("Handled!", error) }) .then(function onFulfilled() { console.log("C!") }) // Will be invoked

Patterns

From the above examples we can see there is a lot of different ways that we can consume promises, so lets define some patterns that can be used throughout your code!

Resolve & Reject

In all our examples we utilised Promise.resolve and Promise.reject. These are great functions to have on our toolbelt because it allows us to return values early if we don't need to do extra processing. This is especially helpful when we want our functions to return a promise no matter what. For example, if we wanted to throw an error early, or resolve our value from a cache if it is available:

js
const cache = {}; function runSomeStuff(id, value) { // Do some constraint checking if (typeof id !== "string" || typeof value !== "string") { return Promise.reject(new Error("One or more argument was not a string!")) } if (cache[id]) { return Promise.resolve(cache[id]); } // Returns a promise const promise = runSomeOtherStuff(id, value); // Doesn't change the returned promise promise.then(function onFulfilled(newValue) { cache[id] = newValue }); return promise; }

Now when I run runSomeStuff I expect to receive a promise no matter what happens.

Promise.resolve and Promise.reject are handy escapes for when you don't want to construct a promise just to return an already known value, which brings us to the next section!

Constructing a promise

In all our previous examples we have created promises only using Promise.resolve and Promise.reject, but it is possible to create your own promise using the Promise constructor, the constructor accepts a single function argument. This function can be thought of as the executor, which will run our asynchronous code and fulfil or reject the promise with a value as a result of the executed code.

The executor function is invoked with two arguments, resolve and reject (both accepting only a single value argument). When resolve is invoked with a value, the promise will be resolved with said value, and when reject is invoked with a value, it will be rejected with said value.

The executor function is like a handler. If an error is thrown in the executor, the promise will be rejected with said error. The executor function is a little different from the handler though, as the returned value is ignored and does not affect how the promise is fulfilled.

The construction process with the executor can be visualised with this function:

js
function invokeExecutor(executor, resolve, reject) { try { executor.call(undefined, resolve, reject); } catch(error) { reject(error); } }

The resolve and reject functions can be only invoked once, any time the function is invoked then after will be ignored. This also means that if the resolve function was invoked, any calls to reject will be ignored, and vice-versa.

With the above knowledge we can now create promises!

The promise defined in this example will be fulfilled after 3 seconds:

js
new Promise(function executor(resolve) { setTimeout(resolve, 3000) }) .then(function onFulfilled() { console.log('It has been 3 seconds!') });

Because then, catch, and finally are all chainable, we can consume our promise immediately if required, as shown in the above example.

Serial

We can resolve a bunch of promises in a serial fashion if we utilise Array.prototype.reduce, where the initial value is an already resolved promise, and the returned value will be a promise:

js
const waitTimes = [1000, 500, 200, 300]; const waitedPromise = waitTimes .reduce( function reducer(previousPromise, waitTime) { // Ensure we always return the result of our chained promises! return previousPromise .then(function onFulfilled(previousValue) { // The previousValue will either be the initial value, or the result of the last // handler to be invoked // This function will only be invoked if all previous functions were fulfilled // // Do what we need to here // We can use the value waitTime from our scope, this will be fixed as // the value from the array console.log(`Waiting ${waitTime} milliseconds!`, previousValue); // Any argument passed to setTimeout after the first 2 will be passed as arguments to the first argument // after the timeout! return new Promise(resolve => setTimeout(resolve, waitTime, previousValue + 1)) }) }, // If you want an initial value that can be used within your promises, // pass it as an argument here, this will map to `previousValue` Promise.resolve(42) ); waitedPromise.then(() => console.log("All done!"));

This can be a handy pattern to have if you're wanting to do any task in serial, but the tasks are all asynchronous. This can also be achieved using the npm module promise.series (this module implements the above pattern one to one):

js
import series from 'promise.series' const waitTimes = [1000, 500, 200, 300]; const waitedPromise = series( // The first argument is an array of functions that will be invoked in series waitTimes.map(function createExecuteFunction(waitTime) { // Return a promise that will result in a promise return function execute(previousValue) { console.log(`Waiting ${waitTime} milliseconds!`, previousValue); return new Promise(function executor(resolve) { setTimeout(resolve, waitTime, previousValue + 1) }) } }), // The second argument passed will be the initial value passed to the first function 42 ); waitedPromise.then(function onFulfilled() { console.log("All done!") });

Concurrent

If you have a bunch of promises that you want to wait to resolve, but don't care about the order they resolve in, then you can utilise Promise.all. This function accepts an array (or iterable) of values, and returns promise where the value will be fulfilled to an array with the values of the provided promises once they are fulfilled. If any of the provided promises reject then the resulting promise will be rejected with the error from the first rejected promise. If more than one promise is rejected, then only the first rejection will be reported.

This example is different from the previous. Instead of waiting for each promise to resolve one after each other, we're waiting for all promises to be in a fulfilled state. This means that the code will be completed after 1000 milliseconds, rather than the 2000 milliseconds it took in the previous example (a sum of all wait times).

Another thing to note in this example is that the values match the order of the original promise, so the first value in the array will always correspond to the fulfilled value from the first promise. When running the example we will see "All done!" [0, 1, 2, 3] logged to our console, where [0, 1, 2, 3] is from the index value passed to setTimeout:

js
const waitTimes = [1000, 500, 200, 300]; const waitedPromise = Promise.all( waitTimes.map( (waitTime, index) => new Promise( resolve => setTimeout(resolve, waitTime, index) ) ) ); waitedPromise.then((values) => console.log("All done!", values));

This is great for instances where we have a bunch of promises doing different things, but we want to consume their values all at once:

js
function getStoreForName(name) { // This function would do some async task return new Promise(function executor(resolve) { setTimeout(resolve, 42, { name }) }); } Promise.all([ getStoreForName("Users"), getStoreForName("Products") ]) .then(function onFulfilled([ userStore, productStore ]) { // Do something with userStore & productStore here console.log({ userStore, productStore }); });

Something to know with this function is that if you pass an empty array then the promise will be resolved with an empty array.This is also important to know when dealing with race as they treat this case slightly different.

Race

Promise.race likes to the Promise.all function described above, yet instead of waiting for all promises to resolve, it will complete as soon as the first promise is fulfilled or rejected.

If no promises are passed, for example in the instance of an empty array, the returned promise will never be fulfilled or rejected.

This function might be used in a case where you have many ways to handle a value, where the handling functions will only resolve their promise if they can handle said value:

js
const value = "this is my value!"; const handleA = (value) => new Promise(function executor(resolve) { if (value.indexOf("this") > -1) { return; } setTimeout(resolve, 200, "A!"); }); const handleB = (value) => new Promise(function executor(resolve) { if (value.indexOf("these") > -1) { return; } setTimeout(resolve, 100, "B"); }); // This one handles everything, but takes a little longer const handleC = (value) => new Promise(function executor(resolve) { setTimeout(resolve, 300, "C!") }); Promise.race([ handleA(value), handleB(value), handleC(value) ]) .then(function onFulfilled(newValue) { console.log({ newValue }) });

Promisify

If we're not utilising promises with asynchronous code, then we might be using the callback pattern. This is a very straightforward pattern where the final argument (the callback isn't always the final argument, but the majority of the time this is the case) passed is a function that is invoked once the asynchronous code has been completed.

The most common signature of the callback function is callback(error, result = undefined). If the error argument is provided then it is assumed that something went wrong in the async code and we must handle the error. If the error is null or undefined, then we can assume the async code ran as expected and we can utilise the result.

This can be seen a lot in the Node.js API's. For example the fs.readFile function accepts a callback as described above:

js
const fs = require("fs"); fs.readFile("package.json", "utf-8", function onComplete(error, data) { if (error) { console.warn("Something went wrong!", error); return; } console.log({ data }) });

We could map this to the promise syntax if we invoke this code within our executor:

js
const fs = require("fs"); new Promise( (resolve, reject) => fs.readFile( "package.json", "utf-8", function onComplete(error, data) { if(error) return reject(error); resolve(data) } ) ) .then(data => console.log({ data })) .catch(error => console.warn("Something went wrong!", error));

The above two examples are functionally identical; except with the promise example we can utilise our promises convention, meaning we don't need to mix more than one style of invoking async code across our codebase.

If we didn't want to construct our promises each time, we can create a function that automatically does this for us:

js
function promisify(fn) { return (...args) => { return new Promise( function executor(resolve, reject){ // Add our callback as the final argument fn(...args, function onComplete(error, result) { if(error) return reject(error); resolve(result) }) } ); }; }

Now we can use this function to promisify our functions:

js
const fs = require('fs'), readFile = promisify(fs.readFile); readFile("package.json", "utf-8") .then(function onFulfilled(data) { console.log({ data }) }) .catch(function onRejected(error) { console.warn("Something went wrong!", error) });

This same functionality can be achieved using Node.js's util.promisify function, or using the es6-promisify npm module. With either of these utilities you can achieve the same as our self defined promisify function, but these utility functions take into account small extra functionality which can be read about in their documentation.

async & await

The async and await syntax is an extension of everything mentioned in this article. Before we can utilise the syntax it is best to understand the above, which is why we mention these last.

We can append the async keyword when defining our functions, which will ensure the returned value of our function is always a promise. It also enables the usage of await, which can be used to wait for a promise to be resolved:

js
async function run() { const myValue = await Promise.resolve(42); console.log(myValue); // Logs 42 } run() .then(function onFulfilled() { console.log("Complete!") })

If we throw an error in our async function, then the resulting promise will be rejected:

js
async function run() { throw new Error("I threw an error"); } run() .catch(function onCatch(error) { console.warn(error) });

This is also the same if we used the await syntax with a promise that rejects:

js
async function run() { await Promise.reject(new Error("I threw an error")); } run() .catch(function onCatch(error) { console.warn(error) });

This makes promises a very simple pattern to work with, because our code is flattened as if it was written in a synchronous fashion.

Following are a couple examples of its usage, showing how using await cleans up your codebase a bunch!

setTimeout

js
async function sleep(time) { return new Promise(resolve => setTimeout(resolve, time)); } async function run() { console.log("About to wait for 1 second!"); await sleep(1000); console.log("After 1 second!"); } run() .then(function onFulfilled() { console.log("Complete!") })

Node.js readFile

js
const { promisify } = require('util'), { readFile } = require('fs'); async function run() { const data = await promisify(readFile)("package.json", "utf-8"); console.log({ data }); } run() .then(function onFulfilled() { console.log("Complete!") })

Promise.all

js
function getStoreForName(name) { // This function would do some async task return new Promise(resolve => setTimeout(resolve, 42, { name })); } async function run() { const [ userStore, productStore ] = await Promise.all([ getStoreForName("Users"), getStoreForName("Products") ]); // Do something with userStore & productStore here console.log({ userStore, productStore }); } run() .then(function onFulfilled() { console.log("Complete!") })
Read similar articles