Dirk Harriman Banner Image

 

Notes Javascript - Promises


 

 

Promises

From Bard

A JavaScript promise is an object that represents the eventual completion or failure of an asynchronous operation.
It is a way to handle asynchronous code in a more structured and predictable way.

Promises are created using the Promise constructor. The constructor takes a function as an argument, which is called the executor function. The executor function takes two arguments: a resolve function and a reject function. The resolve function is called when the asynchronous operation completes successfully, and the reject function is called when the asynchronous operation fails.

Once a promise is created, it can be resolved or rejected using the resolve and reject methods. The resolve method takes a value as an argument, and the reject method takes an error as an argument.

Promises can be chained together using the then method. The then method takes two functions as arguments: a success function and a failure function. The success function is called when the promise is resolved, and the failure function is called when the promise is rejected.

The following code shows how to create a promise and chain it together with another promise:

const promise1 = new Promise((resolve, reject) => { // DO SOMETHING ASYNCHRONOUS resolve("Success!"); }); const promise2 = promise1.then((value) => { // DO SOMETHING WITH THE VALUE console.log(value); });

In this code, the promise1 promise is created and resolved with the value "Success!". The promise2 promise is then chained to the promise1 promise. The promise2 promise will be resolved with the value "Success!" when the promise1 promise is resolved.

Promises are a powerful tool for handling asynchronous code in JavaScript. They make it easier to write code that is more predictable and easier to read.

Error Handling

const promise1 = new Promise((resolve, reject) => { // DO SOMETHING ASYNCHRONOUS try { // THIS WILL THROW AN ERROR 1 / 0; } catch (error) { reject(error); } }); promise1.then((value) => { // THIS WILL NEVER BE CALLED BECAUSE THE PROMISE WAS REJECTED // IT WILL ONLY BE CALLED IN A SUCCESSFUL OPERATION console.log(value); }, (error) => { // THIS WILL BE CALLED BECAUSE THE PROMISE WAS REJECTED console.log(error); });

In this code, the promise1 promise is created and rejected with an error. The then method is then used to handle the error. The success function will never be called because the promise was rejected. The failure function will be called and will log the error.

Here is another example of how to handle errors with promises:

const promise1 = new Promise((resolve, reject) => { // DO SOMETHING ASYNCHRONOUS fetch("https://example.com").then((response) => { if (response.status === 200) { resolve(response.json()); } else { reject(new Error("Error fetching data")); } }); }); promise1.then((data) => { // THIS WILL BE CALLED IF THE PROMISE WAS RESOLVED console.log(data); }, (error) => { // THIS WILL BE CALLED IF THE PROMISE WAS REJECTED console.log(error); });

In this code, the promise1 promise is created and used to fetch data from a web server.
The then method is then used to handle the success and failure cases.
The success function will be called if the request was successful and will log the data.
The failure function will be called if the request failed and will log the error.

Promises are a powerful tool for handling errors in JavaScript. They make it easier to write code that is more predictable and easier to read.

Real Life Example of Promise Chains

A real life example of using Javascript Promise for multiple asynchronous calls

// FETCH THE USER FROM THE DATABASE const user = await getUser(); // FETCH THE USER'S PROFILE IMAGE const profileImage = await getProfileImage(user.id); // UPLOAD THE PROFILE IMAGE TO THE SERVER await uploadProfileImage(profileImage); // SEND AN EMAIL TO THE USER CONFIRMING THE CHANGES await sendEmailConfirmation(user);

In this example, we are performing three asynchronous operations: fetching the user from the database, fetching the user's profile image, and uploading the profile image to the server. We can chain these operations together using promises, which ensures that they are executed in the correct order.

Await & Async
 

The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value. A Promise is a proxy for a value not necessarily known when the promise is created.
It allows you to associate handlers with an asynchronous action's eventual success value or failure reason.
This lets asynchronous methods return values like synchronous methods:
instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future.

A Promise is in one of these states:

The eventual state of a pending promise can either be fulfilled with a value or rejected with a reason (error).
When either of these options occur, the associated handlers queued up by a promise's then method are called.
If the promise has already been fulfilled or rejected when a corresponding handler is attached, the handler will be called, so there is no race condition between an asynchronous operation completing and its handlers being attached.

A promise is said to be settled if it is either fulfilled or rejected, but not pending.

Essentially, a promise is a returned object to which you attach callbacks, instead of passing callbacks into a function. Imagine a function, createAudioFileAsync(), which asynchronously generates a sound file given a configuration record and two callback functions: one called if the audio file is successfully created, and the other called if an error occurs.

new Promise(executor);

Parameters

executor - A function to be executed by the constructor.
It receives two functions as parameters: resolveFunction and rejectFunction. The executor is custom code that ties an outcome in a callback to a promise. The executor's signature is expected to be something like:

function executor(resolveFunction, rejectFunction) { // Typically, some asynchronous operation that accepts a callback. }

Any errors thrown in the executor will cause the promise to be rejected, and the return value will be neglected.

Return Value

When called via new, the Promise constructor returns a promise object.
The promise object will become resolved when either of the functions resolveFunc or rejectFunc are invoked.
Note that if you call resolveFunc or rejectFunc and pass another Promise object as an argument, it can be said to be "resolved", but still not "settled".


createAudioFileAsync(audioSettings, successCallback, failureCallback); function successCallback(result) { console.log('Audio file ready at URL: ${result}'); } function failureCallback(error) { console.error('Error generating audio file: ${error}'); }

If createAudioFileAsync() were rewritten to return a promise, you would attach your callbacks to it instead:

createAudioFileAsync(audioSettings).then(successCallback, failureCallback); function successCallback(result) { console.log('Audio file ready at URL: ${result}'); } function failureCallback(error) { console.error('Error generating audio file: ${error}'); }


 

When & How To Use Promises

JavaScript is single threaded, meaning that two bits of script cannot run at the same time; they have to run one after another sequentially. In browsers, JavaScript shares a thread with a load of other stuff that differs from browser to browser. But typically JavaScript is in the same queue as painting, updating styles, and handling user actions (such as highlighting text and interacting with form controls). Activity in one of these things delays the others.

var img1 = document.querySelector(".img-1"); img1.addEventListener('load', function(){ // IMAGE HAS BEEN LOADED }); img1.addEventListener('error', function(){ // AN ERROR HAS OCCURED, IMAGE HAS NOT BEEN LOADED });

A JavaScript Promise object contains both the producing code and calls to the consuming code:

let myPromise = new Promise(function(myResolve, myReject) { // "PRODUCING CODE" (MAY TAKE SOME TIME) myResolve(); // when successful myReject(); // when error }); // "CONSUMING CODE" (MUST WAIT FOR A FULFILLED PROMISE) myPromise.then( function(value) { /* code if successful */ }, function(error) { /* code if some error */ } );

myPromise.then( function(value) { /* code if successful */ }, function(error) { /* code if some error */ } );

function runPromise1() { var divResult = document.getElementById("results1"); let myPromise = new Promise(function(myResolve, myReject) { let x = 0; // THIS WOULD BE THE RESULT OF SOME ACTION if (x == 0) { myResolve("OK"); } else { myReject("Error"); } }); myPromise.then( function(value) {myDisplayer(value);}, function(error) {myDisplayer(error);} ); } function runPromise2() { var divResult = document.getElementById("results1"); let myPromise = new Promise(function(myResolve, myReject) { let x = 5; // THIS WOULD BE THE RESULT OF SOME ACTION if (x == 0) { myResolve("OK"); } else { myReject("Error"); } }); myPromise.then( function(value) {myDisplayer(value);}, function(error) {myDisplayer(error);} ); } function myDisplayer(strMessage) { document.getElementById("results1").innerHTML = strMessage; }

Clicking on the first button will run the script runPromise1() in which the result of the action where x = 0 causes the promise to call on the success function.
Clicking on the second button will run the script runPromise2() in which the result of the action where x = 5 causes the promise to call on the failure function.

Run With x = 0  Run With x = 5
 

Response


 

Sample

function runPromise3(fileName) { var divResult = document.getElementById("results3"); let myPromise = new Promise(function(myResolve, myReject) { let req = new XMLHttpRequest(); req.open('GET', fileName); req.onload = function() { if (req.status == 200) { myResolve(req.response); } else { myReject("File not Found"); } }; req.send(); }); myPromise.then( function(value) {myDisplayer3(value);}, function(error) {myDisplayer3(error);} ); } function myDisplayer3(some) { document.getElementById("results3").innerHTML = some; }

Sample With Existing HTML  Sample Without Existing HTML 
 

Response


 

 

The Pyramid of Doom

A common need is to execute two or more asynchronous operations back to back, where each subsequent operation starts when the previous operation succeeds, with the result from the previous step.
In the old days, doing several asynchronous operations in a row would lead to the classic callback pyramid of doom:

/// THE PYRAMID OF DOOM doSomething(function (result) { doSomethingElse(result, function (newResult) { doThirdThing(newResult, function (finalResult) { console.log('Got the final result: ${finalResult}'); }, failureCallback); }, failureCallback); }, failureCallback);

Chaining

With promises, we accomplish this by creating a promise chain.
The API design of promises makes this great, because callbacks are attached to the returned promise object, instead of being passed into a function.

Here's the magic: the then() function returns a new promise, different from the original:

const promise = doSomething(); const promise2 = promise.then(successCallback, failureCallback);

This second promise promise2 represents the completion not just of doSomething(), but also of the successCallback or failureCallback you passed in, which can be other asynchronous functions returning a promise. When that's the case, any callbacks added to promise2 get queued behind the promise returned by either successCallback or failureCallback.

With this pattern, you can create longer chains of processing, where each promise represents the completion of one asynchronous step in the chain. In addition, the arguments to then are optional, and catch(failureCallback) is short for then(null, failureCallback), so if your error handling code is the same for all steps, you can attach it to the end of the chain:

Longer Chains of Processing

doSomething() .then(function (result) { return doSomethingElse(result); }) .then(function (newResult) { return doThirdThing(newResult); }) .then(function (finalResult) { console.log('Got the final result: ${finalResult}'); }) .catch(failureCallback);

Arrow Functions

This can also be expressed using arrow functions

doSomething() .then((result) => doSomethingElse(result)) .then((newResult) => doThirdThing(newResult)) .then((finalResult) => { console.log('Got the final result: ${finalResult}'); }) .catch(failureCallback);

The Floating Promise Problem

Important Note: Always return results, otherwise callbacks won't catch the result of a previous promise (with arrow functions, () => x is short for () => { return x; }). If the previous handler started a promise but did not return it, there's no way to track its settlement anymore, and the promise is said to be "floating".

doSomething() .then((url) => { // I forgot to return this fetch(url); }) .then((result) => { // RESULT IS UNDEFINED, BECAUSE NOTHING IS RETURNED FROM // THE PREVIOUS HANDLER. // THERE'S NO WAY TO KNOW THE RETURN VALUE OF THE fetch() // CALL ANYMORE, OR WHETHER IT SUCCEEDED AT ALL. });

Race Conditions

This may be worse if you have race conditions, if the promise from the last handler is not returned, the next then handler will be called early, and any value it reads may be incomplete.

const listOfIngredients = []; doSomething() .then((url) => { // I FORGOT TO RETURN THIS fetch(url) .then((res) => res.json()) .then((data) => { listOfIngredients.push(data); }); }) .then(() => { console.log(listOfIngredients); // ALWAYS [], BECAUSE THE FETCH REQUEST HASN'T COMPLETED YET. });

Therefore, as a rule of thumb, whenever your operation encounters a promise, return it and defer its handling to the next then handler.

const listOfIngredients = []; doSomething() .then((url) => fetch(url) .then((res) => res.json()) .then((data) => { listOfIngredients.push(data); }), ) .then(() => { console.log(listOfIngredients); }); // OR doSomething() .then((url) => fetch(url)) .then((res) => res.json()) .then((data) => { listOfIngredients.push(data); }) .then(() => { console.log(listOfIngredients); });

Nesting

In the two examples above, the first one has one promise chain nested in the return value of another then() handler, while the second one uses an entirely flat chain. Simple promise chains are best kept flat without nesting, as nesting can be a result of careless composition. See common mistakes.

Nesting is a control structure to limit the scope of catch statements. Specifically, a nested catch only catches failures in its scope and below, not errors higher up in the chain outside the nested scope. When used correctly, this gives greater precision in error recovery:

doSomethingCritical() .then((result) => doSomethingOptional(result) .then((optionalResult) => doSomethingExtraNice(optionalResult)) .catch((e) => {}), ) // IGNORE IF OPTIONAL STUFF FAILS; PROCEED. .then(() => moreCriticalStuff()) .catch((e) => console.error('Critical failure: ${e.message}'));

Note that the optional steps here are nested, with the nesting caused not by the indentation, but by the placement of the outer ( and ) parentheses around the steps.

The inner error-silencing catch handler only catches failures from doSomethingOptional() and doSomethingExtraNice(), after which the code resumes with moreCriticalStuff().
Importantly, if doSomethingCritical() fails, its error is caught by the final (outer) catch only, and does not get swallowed by the inner catch handler.

Chaining After A Catch

It's possible to chain after a failure, i.e. a catch, which is useful to accomplish new actions even after an action failed in the chain. Read the following example:

new Promise((resolve, reject) => { console.log("Initial"); resolve(); }) .then(() => { throw new Error("Something failed"); console.log("Do this"); }) .catch(() => { console.error("Do that"); }) .then(() => { console.log("Do this, no matter what happened before"); });

This will output the following text:

Initial Do that Do this, no matter what happened before

Chained Promise
 

Response

function runPromise1() { var divResult = document.getElementById("results1"); new Promise((resolve, reject) => { divResult.innerHTML = "Initial"; resolve(); }) .then(() => { throw new Error("Something failed"); divResult.innerHTML += "<br/>Do this"; }) .catch(() => { divResult.innerHTML += "<br/>Do that"; }) .then(() => { divResult.innerHTML += "<br/>Do this, no matter what happened before"; }); } // RESPONSE Initial Do that Do this, no matter what happened before

Code Trace


 

Working Examples

Example 1

/******************************************************************************/ function resolveAfter2Seconds() { console.log("starting slow promise"); outputDiv1.innerHTML += "Starting Slow Promise - resolveAfter2Seconds()<br/>"; return new Promise((resolve) => { setTimeout(() => { resolve("slow"); console.log("slow promise is done"); outputDiv1.innerHTML += "Slow Promise Is Done - resolveAfter2Seconds()<br/>"; }, 2000); }); } /******************************************************************************/ function resolveAfter1Second() { console.log("starting fast promise"); outputDiv1.innerHTML += "Starting Fast Promise - resolveAfter1Second()<br/>"; return new Promise((resolve) => { setTimeout(() => { resolve("fast"); console.log("fast promise is done"); outputDiv1.innerHTML += "Fast Promise Is Done - resolveAfter1Second()<br/>"; }, 1000); }); } /******************************************************************************/ async function sequentialStart() { console.log("== sequentialStart starts =="); outputDiv1.innerHTML += "** sequentialStart() Starts **<br/>"; const slow = resolveAfter2Seconds(); // 1. START A TIMER, LOG AFTER IT'S DONE console.log(await slow); outputDiv1.innerHTML += "Slow - sequentialStart()<br/>"; const fast = resolveAfter1Second(); // 2. START THE NEXT TIMER AFTER WAITING FOR THE PREVIOUS ONE console.log(await fast); outputDiv1.innerHTML += "fast<br/>"; console.log("== sequentialStart done =="); outputDiv1.innerHTML += "** sequentialStart() Ends **<br/>"; } /******************************************************************************/ async function sequentialWait() { console.log("== sequentialWait starts =="); outputDiv1.innerHTML += "** sequentialWait() Starts **<br/>"; const slow = resolveAfter2Seconds(); // 1. START TWO TIMERS WITHOUT WAITING FOR EACH OTHER const fast = resolveAfter1Second(); console.log(await slow); // 2. WAIT FOR THE SLOW TIMER TO COMPLETE, AND THEN LOG THE RESULT outputDiv1.innerHTML += "Slow - sequentialWait()<br/>"; console.log(await fast); // 3. WAIT FOR THE FAST TIMER TO COMPLETE, AND THEN LOG THE RESULT outputDiv1.innerHTML += "Fast - sequentialWait()<br/>"; console.log("== sequentialWait done =="); outputDiv1.innerHTML += "** sequentialWait() Ends **<br/>"; } /******************************************************************************/ async function concurrent1() { console.log("== concurrent1 starts =="); outputDiv1.innerHTML += "** concurrent1() Starts **<br/>"; // 1. START TWO TIMERS CONCURRENTLY AND WAIT FOR BOTH TO COMPLETE const results = await Promise.all([ resolveAfter2Seconds(), resolveAfter1Second(), ]); // 2. LOG THE RESULTS TOGETHER console.log(results[0]); outputDiv1.innerHTML += "results[0]: "+ results[0] +"<br/>"; console.log(results[1]); outputDiv1.innerHTML += "results[1]: "+ results[1] +"<br/>"; console.log("== concurrent1 done =="); outputDiv1.innerHTML += "** concurrent1() Ends **<br/>"; } /******************************************************************************/ async function concurrent2() { console.log("== concurrent2 starts =="); outputDiv1.innerHTML += "** concurrent2 Starts **<br/>"; // 1. START TWO TIMERS CONCURRENTLY, LOG IMMEDIATELY AFTER EACH ONE IS DONE await Promise.all([ (async () => console.log(await resolveAfter2Seconds()))(), (async () => console.log(await resolveAfter1Second()))(), ]); console.log("== concurrent2 done =="); outputDiv1.innerHTML += "** concurrent2 Ends **<br/>"; } let runExample1 = function() { outputDiv1.innerHTML = "Starting Example - Calling sequentialStart()<br/>"; sequentialStart(); // AFTER 2 SECONDS, LOGS "slow", THEN AFTER 1 MORE SECOND, "fast" // WAIT ABOVE TO FINISH outputDiv1.innerHTML += "After 4000 Calling sequentialWait()<br/>"; setTimeout(sequentialWait, 4000); // AFTER 2 SECONDS, LOGS "slow" AND THEN "fast" // WAIT AGAIN outputDiv1.innerHTML += "After 7000 Calling concurrent1()<br/>"; setTimeout(concurrent1, 7000); // SAME AS sequentialWait // WAIT AGAIN outputDiv1.innerHTML += "After 10000 Calling concurrent2()<br/>"; setTimeout(concurrent2, 10000); // AFTER 1 SECOND, LOGS "fast", THEN AFTER 1 MORE SECOND, "slow" };

Run Example 1
 

Response 1


 
Example 2

const userLeft = true; const userWatchingCatMeme = false; function watchTutorialCallback(callback, errorCallback) { if (userLeft) { errorCallback({ name: "User Left", message: "No one is home"}); } else if (userWatchingCatMeme) { errorCallback({ name: "User Watching Cat Meme", message: "User waisting time"}); } else { callback("User ready to go"); } } let runExample2 = function() { outputDiv2.innerHTML = "Starting<br/>"; watchTutorialCallback((message) => { console.log("Success: "+ message); outputDiv2.innerHTML = "Success: "+ message +"<br/>"; }, (error) => { console.log(error.name +" "+ error.message); outputDiv2.innerHTML = "Error: "+ error.name +" - "+ error.message +"<br/>"; }) };

Run Example 2
 

Response 2


 
Example 3

function watchTutorialPromise() { return new Promise((resolve, reject) => { if (userLeft) { reject({ name: "User Left", message: "No One Is Home"}); } else if (userWatchingCatMeme) { reject({ name: "User Watching Cat Meme", message: "User Waisting Time"}); } else { resolve("User Ready To Go"); } }) } let runExample3 = function() { outputDiv3.innerHTML = "Starting<br/>"; watchTutorialPromise().then((message) => { console.log("Success: "+ message); outputDiv3.innerHTML = "Success: "+ message; }).catch((error) => { console.log(error.name +" "+ error.message); outputDiv3.innerHTML = "Error: "+ error.name +" "+ error.message; }) };

Run Example 3
 

Response 3


 
Example 4

In this example the Promise method all() is called. This processes all items in the given array.
There is another method called race() which will only process the item which finishes first.

const recordVideoOne = new Promise((resolve, reject) => { resolve('Video 1 Recorded'); }) const recordVideoTwo = new Promise((resolve, reject) => { resolve('Video 2 Recorded'); }) const recordVideoThree = new Promise((resolve, reject) => { resolve('Video 3 Recorded'); }) let runExample4 = function() { outputDiv4.innerHTML = "Starting<br/>"; Promise.all([ recordVideoOne, recordVideoTwo, recordVideoThree ]).then((messages) => { console.log(messages); outputDiv4.innerHTML += "messages: "+ messages +"<br/>"; }) };

Run Example 4
 

Response 4