In this article we examine promises, a fundamental concept in the asynchronous world of javascript.
The concept of a promise in javascript involves two parties:
For example, let’s imagine me and you having a conversation:
You: I want to understand javascript promises better.
Me: I promise you that once you read this article, you will have a better understanding of javascript promises.
You: Ok Alex, once I finish reading, if I understand better I will comment 😀 otherwise I will comment 😕 .
You asked for something and I promised I could deliver it. You then told me how the outcome of the promise would shape your action.
Keep in mind, the promise giver and consumer have differnt objectives. The promise giver acts upon his/her promise and produces an outcome for that promise. The promise consumer is given an outcome and proceeds accordingly.
In this case the promise giver (me) has to make sure the reader understands promises after reading this article. The promise receiver, after reading the article will receiving an outcome which is either undestanding or not understanding ‘promises’. He/she will then proceed to comment 😀 if promises were understood or comment 😕 if promises are still a mystery.
A promise begins its lifetime in the pending
state.
When a promise reaches a successful outcome, it is moves into the fulfilled
state (or the promise is fulfilled).
When a promise fails to get the desired outcome it moves into the rejected
state. (or the promise is rejected).
In either case, when a promise is fulfilled or rejected, the promise is said to be settled
and the state is irreversible.
When a promise is fulfilled it usually produces an outcome, which is called the value
of the promise. It is said a promise is fulfilled with a certain value
.
When a promise gets rejected it usually throws an error, which is called the reason
of the promise. It is said a promise is rejected for a certain reason
.
So with this conceptual model in mind, let’s try to understand promises better.
Promises are created by invoking the Promise constructor:
new Promise(executor)
The function understandPromises
below returns a promise.
function understandPromises(article){ return new Promise((fulFill, reject) => { const understanding = readArticle(article); if (understanding){ fulFill(😀); } else { reject(😕); } }); }
The constructor receives a parameter, namely the executor
function which will run synchronously immediatetly after the new Promise()
expression is run.
The executor settles the promise either by fulfilling it or rejecting it.
This is possible because the executor receives two functions as parameters one that fulfills and one that rejects the promise. Each of this functions receives the value
and reason
as parameters respectively.
Inside the body of the executor the logic of the asynchronous operation is defined. This logic determines when the fulfill and reject functions will be called.
If you want to know exactly what happens once the new Promise()
runs, I recommend you read the Description of the MDN page ‘Promise() constructor’.
So once a promise created, it will eventually be settled, and depending whether the promise was fulfilled or rejected, the consumer will act accordingly.
Binding a set of actions to the settlement is done by attaching the instance method .then()
to the given promise.
const onFulfill = (value) => { console.log(value); }; const onReject = (reason) => { throw new Error(reason); }; understandPromises(article).then(onFulfill, onReject);
In the above operation, understandPromises
returns a promise. By calling .then()
on the returned promise, we can handle the fulfillement and rejection scenarios of the promise.
When the promise is “fulfilled”, onFulfill
is called with the parameter “value” which is the value returned by the successful operation. When the promise is “rejected” onReject
is called and has access to the “reason” of the error.
The method .then() returns a Promise itself, so this allows for Promises to be chained together by the .then()
method .
Also, we have access to the .catch()
promise method that allows to handle all the errors in one place at the end without the need place an onError methods at every .then() .
A .catch()
is literally just this:
somePromise.then(undefined, onError);
The following example illustrates promise chaining:
// get paris coordinates fetchCoordinates("https://example-api.com/geocodes?city=Paris") //then using the coordinates result, get the temperature .then(coords => fetchTemperature(`https://example-api.com/temperature?lat=${coords.lat}&lon=${coords.lon}`) // then process the temperature result .then((temp) => { console.log(`Temperature is ${temp}°C.`) }) // and if somethig goes wrong in any of the promises, throw an error .catch((e) => {throw new Error(e);});
In the example above, we want to obtain the current temperature in the center of Paris and to achieve this we use two APIs: one for obtaining the coordinates of the city center and one for obtaining the temperature at specific coordinates.
So we call the Geocode API first and then use the result as an input for the Temperature API. Any errors thrown by either of the two promise handlers will be handled at the end with the catch method.
There are four static promise methods (called aggregators) that assist when dealing with multiple promises that can be treated in parallel.
They all receive an array of promises as a parameter and return a single promise. The state (fullfiled or rejected) and outcome (value or reason) of the returned promise depends on the type of static method used.
Returned promise gets fulfilled only if all promises are fulfilled. The value of the returned promise is an array with values mapping to each promise.
Returned promise gets rejected if any promise gets rejected. The reason of the returned promise is the reason of the rejected promise.
It is typically used when there are multiple asynchronous tasks that the overall code relies on to work successfully — all of whom we want to fulfill before the code execution continues.
function fetchTemperature(coords) { const tempUrl = `https://example-api.com/temperature?lat=${coords.lat}&lon=${coords.lon}`; return fetch(tempUrl); } function fetchTime(coords) { const timeUrl = `https://example-api.com/time?lat=${coords.lat}&lon=${coords.lon}`; return fetch(tempUrl); } function fetchAltitude(coords) { const timeUrl = `https://example-api.com/time?lat=${coords.lat}&lon=${coords.lon}`; return fetch(tempUrl); } // Promise.all is used to make all calls in parallel // the resulting promise gets fulfilled if all promises fulfill Promise.all(fetchTemperature(coords), fetchTime(coords), fetchAltitude(coords)) .then((results) => { console.log(`${results[0].temp}°C in Paris.`); console.log(`Time in Paris is ${results[1].time}.`); console.log(`Paris is at an altitude of ${results[2].altitude} meters.`); }) .catch((e) => { throw e; });
Returned promise gets fulfilled when all promises are settled. The value of the returned promise is an array of objects that describe how each promise settled. Each object has the properties:
The returned promise cannot be rejected.
Use allSettled()
if you need the final result of every promise in the input iterable.
Returned promise gets fulfilled if any of the promises gets fulfilled. The value of the returned promise is the value of the first promise that gets fulfilled chronologically.
Returned promise gets rejected if all promises gets rejected.
The reason of the returned promise is an AggregateError
.
Returned promise gets fulfilled if the first promise to settle, was fulfilled. The value of the returned promise is the value of that fulfilled promise.
Returned promise gets rejected if the first promise to settle, was fulfilled. The reason of the returned promise is the reason of that rejected promise.
The
async function
declaration declares an async function where theawait
keyword is permitted within the function body. Theasync
andawait
keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.
Async is a special keyword in javascript that is used at function declarations and allows the use of the await keyword within the function.
Await is a special keyword in javscript used with expressions and when used with promises, it “waits” for the promise’s fulfilled value before continuing execution of code. It enables the use of try and catch syntax instead of using promise chains.
async function getTemperature() { try { const dataCoords = await fetch( "https://example-api.com/geocodes?city=Paris" ); const coords = await dataCoords.json(); const dataTemp = await fetch( `https://example-api.com/temperature?lat=${coords.lat}&lon=${coords.lon}` ); const temp = await dataTemp.json(); console.log(`Temperature is ${temp}°C.`); } catch (e) { throw e; } }
Please note that the use of try/catch is essential in order to achieve proper error handling since the abscence of .then()
and .catch()
leaves no other way to handle errors (unless you want to return Promise.reject()
from the async function).
Async/await enables aynchronous operations to be written in a synchronous style but keep in mind that if you want to excute multiple operations that don’t depend on each other, then Promise.all()
will be more efficient since it enables parallel execution.
Async functions always return a promise.
async function get1() { //will return a fulfilled promise with value of 1 return 1; } async function iAmVoid() { // will return a fulfilled promise with value of undefined const x = 5; } async function wait1() { //will return a promise that is fullfiled with value 1. return; await 1; }
Therefore, if you call an async function that returns promise value, that value will be wrapped in a promise which is probably not what you want. To be able to use the result, you should call that async function with await.
function getUsers(){ const users = await db.getUsers(); return users; } //by using await we are able to get the promise value const users = await getUsers(); users.forEach((u) => { console.log(user); })
The reason why we need to use await in order to get the promise value is that the async function immediately returns a promise which eventually will be fulfilled by the await statement.
pending
state. Once it settles
it can be either fulfilled
with a value
or rejected
with a reason
.new Promise(executor)
. Executor receives the fulfill and reject functions as parameters and its body executes the async operation that will eventually be settled..then()
method..catch()
method at the end of the chain.Promise.all()
, Promise.allSettled()
, Promise.any()
and Promise.race()
.async/await
which waits for the promise result before executing the rest of the code synchronously.async/await
the use of try/catch
blocks is recommended.await
.