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:
jsconst 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:
jsconst 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!
:
jsconst 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
:
jsconst 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:
jsimport { 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:
jsfunction 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:
jsfunction 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:
jsconst 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:
jsPromise.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:
jsconst 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:
jsfunction 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:
jsnew 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:
jsconst 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):
jsimport 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
:
jsconst 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:
jsfunction 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:
jsconst 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:
jsconst 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:
jsconst 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:
jsfunction 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:
jsconst 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:
jsasync 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:
jsasync 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:
jsasync 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
jsasync 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
jsconst { 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
jsfunction 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!") })