JavaScript Promise

I have been asked why I am dealing with RESTful services in JavaScript. My idea is to create a set of services that implement sets of APIs (e.g., DICOM storage server) using different programming languages (e.g., C#, Java, and JavaScript). Once that is done will decide on a single service implementation implemented using microservices written in different programming languages. The API for such RESTful server will be determined at the time. As you can see there is some logic behind this madness. If possible, I will then deploy the same set of microservices in a couple public platforms (e.g., AWS and Azure).

In this post I will cover what is a Promise and how to use them in JavaScript. In a nutshell when you have asynchronous operations you wish to execute in parallel you can start them and wait for them to complete and if needed get results from them. If your programming language and operating system allows running multiple processes, thread or fibers, then you could implement similar behaviors. If you are interested in promises, you could read more about them here.

Let’s start by creating a new promise and getting its result back as illustrated by the following code:

// **** create a new promise ****
const p = new Promise((resolve, reject) => {
  // **** start asynchronous work ****

  // **** if all goes well (return result for asynchronous operation) ****
  resolve(1);

  // **** if something goes wrong ****
  // reject(new Error("error message"));
});

// **** ****
p.then(result => console.log("Result: ", result));

Now let’s run it and see what happens:

C:\Users\John>cd async-demo

C:\Users\John\async-demo>dir
 Volume in drive C is OS
 Volume Serial Number is 26E8-87B0

 Directory of C:\Users\John\async-demo

04/24/2019  11:56 AM    <DIR>          .
04/24/2019  11:56 AM    <DIR>          ..
04/24/2019  11:46 AM             1,189 index.js
04/24/2019  08:07 AM               224 package.json
04/24/2019  12:05 PM               371 promise.js       <==== 3 File(s) 1,784 bytes 2 Dir(s) 578,406,711,296 bytes free C:\Users\John\async-demo>nodemon promise.js             <====
[nodemon] 1.18.11
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node promise.js`
Result:  1
[nodemon] clean exit - waiting for changes before restart

We can go back to our code and reject the promise instead of resolving it as illustrated:

// **** create a new promise ****
const p = new Promise((resolve, reject) => {
  // **** start asynchronous work ****
  const delay = 2000;
  setTimeout(() => {
    // **** if all goes well (return result for asynchronous operation) ****
    // resolve(1);    // pending => resolved, fulfilled

    // **** if something goes wrong ****
    reject(new Error("error message :o(")); // pending => rejected
  }, delay);
});

// **** use promise instead of callback instead of asynchronous functions ****
console.log("before");
p.then(result => console.log("result: ", result)).catch(err =>
  console.log("error: ", err.message)
);

console.log("waiting...");

The code will generate the following output:

[nodemon] starting `node promise.js`
before
waiting...
error:  error message :o(
[nodemon] clean exit - waiting for changes before restart

Now let’s experiment returning an object from a promise. A very simple example follows:

// **** create a resolved promise ****
const p = Promise.resolve({ id: 1 });

// **** ****
p.then(result => console.log(result));

The code generates the following output:

[nodemon] starting `node promise-api.js`
{ id: 1 }
[nodemon] clean exit - waiting for changes before restart

As was mentioned before, the promise can be rejected. Let’s see what happens when a promise is rejected:

// **** create a rejected promise ****
const p = Promise.reject(new Error("reason for rejection"));    <==== // **** **** p.catch(error => console.log(error));

When we executed the console produces the following output:

[nodemon] starting `node promise-api.js`
Error: reason for rejection                                     <====
    at Object.<anonymous> (C:\Users\John\async-demo\promise-api.js:8:26)
    at Module._compile (internal/modules/cjs/loader.js:701:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
    at Module.load (internal/modules/cjs/loader.js:600:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
    at Function.Module._load (internal/modules/cjs/loader.js:531:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
[nodemon] clean exit - waiting for changes before restart

As you can see the message is displayed but Node.js displays a stack dump. If we are just performing a single operation we could just wait and do it synchronously. What is we want to make simultaneous API calls to Facebook and Twitter? The following code simulates such case:

// **** simulating calling Facebook API ****
const p1 = new Promise((resolve, reject) => {
  const timeOut = 6000;
  console.log("async operation 1...");
  setTimeout(() => {
    console.log("async operation 1 done!");
    resolve(1);
  }, timeOut);
});

// **** simulating calling Twitter API ****
const p2 = new Promise((resolve, reject) => {
  const timeOut = 3000;
  console.log("async operation 2...");
  setTimeout(() => {
    console.log("async operation 2 done!");
    resolve(2);
  }, timeOut);
});

// **** wait for promises to be resolved ****
Promise.all([p1, p2]).then(result => console.log(result));

Let’s see what happens:

[nodemon] starting `node promise-api.js`
async operation 1...
async operation 2...
async operation 2 done!
async operation 1 done!
[ 1, 2 ]
[nodemon] clean exit - waiting for changes before restart

We start the two operations and then we wait for both to complete before we proceed. This is typically used when you need multiple results to aggregate to return them to a caller. A similar example could be two different queries to two different databases (e.g., MongoDB and SQL Server) and you have to aggregate the results before proceeding.

Of course not all things go as planned. One of the operations may fail. The following code illustrates such scenario:

// **** simulating calling FaceBook API ****
const p1 = new Promise((resolve, reject) => {
  const timeOut = 6000;
  console.log("async operation 1...");
  setTimeout(() => {
    // console.log("async operation 1 done!");
    // resolve(1);
    reject(new Error("something went wrong!")); <==== }, timeOut); }); // **** simulating calling Twitter API **** const p2 = new Promise((resolve, reject) => {
  const timeOut = 3000;
  console.log("async operation 2...");
  setTimeout(() => {
    console.log("async operation 2 done!");
    resolve(2);
  }, timeOut);
});

// **** wait for promises to be resolved ****
Promise.all([p1, p2])
  .then(result => console.log(result))
  .catch(err => console.log("error: ", err.message));

When we execute it we get the following results:

[nodemon] starting `node promise-api.js`
async operation 1...
async operation 2...
async operation 2 done!
error:  something went wrong!   <====
[nodemon] clean exit - waiting for changes before restart

On some occasions you may wish to perform multiple tasks but take the results from the one that completes first. The following example shows such behavior:

// **** simulating calling FaceBook API ****
const p1 = new Promise((resolve, reject) => {
  const timeOut = 6000;
  console.log("async operation 1...");
  setTimeout(() => {
    console.log("async operation 1 done!");
    resolve(1);
    // reject(new Error("something went wrong!"));
  }, timeOut);
});

// **** simulating calling Twitter API ****
const p2 = new Promise((resolve, reject) => {
  const timeOut = 3000;
  console.log("async operation 2...");
  setTimeout(() => {
    console.log("async operation 2 done!");
    resolve(2);
  }, timeOut);
});

// // **** wait for all promises to be resolved ****
// Promise.all([p1, p2])
//   .then(result => console.log(result))
//   .catch(err => console.log("error: ", err.message));

// **** wait for a promise to be resolved ****
Promise.race([p1, p2])
  .then(result => console.log(result))
  .catch(err => console.log("error: ", err.message));

The code would work as follows:

[nodemon] starting `node promise-api.js`
async operation 1...
async operation 2...
async operation 2 done!
2
async operation 1 done!
[nodemon] clean exit - waiting for changes before restart

The following code may be used to replace the promise-based approach found earlier in the code:

/*
 * async and await (similar to C#) approach [3]
 * Must use async function.
 */
async function displayCommits() {
  const user = await getUser(1);
  const repos = await getRepositories(user.gitHubUserName);
  const commits = await getCommits(repos[0]);
  console.log(commits);
}

// **** call the async function ****
displayCommits();

// **** ****
console.log("after");

/*
 * Simulate reading user data from a database.
 */
function getUser(id) {
  // **** return a promise ****
  return new Promise((resolve, reject) => {
    const delay = 2000;
    console.log("getUser <<< reading a user from a database..."); // **** start some async work **** setTimeout(() => {
      console.log("getUser <<< database call done!!!"); resolve({ id: id, gitHubUserName: "JohnCanessa" }); }, delay); }); } /* * Simulate getting repository names using GitHub API. */ function getRepositories(userName) { // **** return a promise **** return new Promise((resolve, reject) => {
    const delay = 2000;
    console.log("getRepositories <<< calling GitHub API..."); // **** start some async work **** setTimeout(() => {
      console.log("getRepositories <<< GitHub API call done!!!"); resolve(["repo1", "repo2", "repo3", "repo4"]); }, delay); }); } /* * Simulate getting commits from a repo using GitHub API. */ function getCommits(repo) { // **** return a promise **** return new Promise((resolve, reject) => {
    const delay = 2000;
    console.log("getCommits <<< calling GitHub API..."); // **** start some async work **** setTimeout(() => {
      console.log("getCommits <<< GitHub API call done!!!");
      resolve(["commit"]);
    }, delay);
  });
}

The code is simpler and easier to follow and debug. A run of it follows:

[nodemon] starting `node index.js`
before
getUser <<< reading a user from a database...
after
getUser <<< database call done!!!
getRepositories <<< calling GitHub API...
getRepositories <<< GitHub API call done!!!
getCommits <<< calling GitHub API...
getCommits <<< GitHub API call done!!!
[ 'commit' ]
[nodemon] clean exit - waiting for changes before restart

The following is the completed test code for the async and await set. The await needs to be enclosed in an async function. In this case a failure is induced in the getRepositories() function.  The set of calls is enclosed in a try-catch set of blocks. This allows us to catch an error if it occurs.  This is illustrated in the following code:

/*
 * async and await (similar to C#) approach [3]
 * Must use async function.
 */
async function displayCommits() {
  // **** try block ****
  try {
    const user = await getUser(1);
    const repos = await getRepositories(user.gitHubUserName);
    const commits = await getCommits(repos[0]);
    console.log(commits);
  } catch (err) {
    // **** catch block ****
    console.log("error", err.message);
  }
}

// **** call the async function ****
displayCommits();

// **** ****
console.log("after");

/*
 * Simulate reading user data from a database.
 */
function getUser(id) {
  // **** return a promise ****
  return new Promise((resolve, reject) => {
    const delay = 2000;
    console.log("getUser <<< reading a user from a database..."); // **** start some async work **** setTimeout(() => {
      console.log("getUser <<< database call done!!!"); resolve({ id: id, gitHubUserName: "JohnCanessa" }); }, delay); }); } /* * Simulate getting repository names using GitHub API. */ function getRepositories(userName) { // **** return a promise **** return new Promise((resolve, reject) => {
    const delay = 2000;
    console.log("getRepositories <<< calling GitHub API..."); // **** start some async work **** setTimeout(() => {
      console.log("getRepositories <<< GitHub API call done!!!"); // resolve(["repo1", "repo2", "repo3", "repo4"]); reject(new Error("failed getting repos :o(")); }, delay); }); } /* * Simulate getting commits from a repo using GitHub API. */ function getCommits(repo) { // **** return a promise **** return new Promise((resolve, reject) => {
    const delay = 2000;
    console.log("getCommits <<< calling GitHub API..."); // **** start some async work **** setTimeout(() => {
      console.log("getCommits <<< GitHub API call done!!!");
      resolve(["commit"]);
    }, delay);
  });
}

The actual run follows:

[nodemon] starting `node index.js`
before
getUser <<< reading a user from a database...
after
getUser <<< database call done!!!
getRepositories <<< calling GitHub API...
getRepositories <<< GitHub API call done!!!
error failed getting repos :o(
[nodemon] clean exit - waiting for changes before restart

I have uploaded the set of relevant files to my GitHub repository.

BTW the contents of the package.json file follows:

C:\Users\John\async-demo>dir
 Volume in drive C is OS
 Volume Serial Number is 26E8-87B0

 Directory of C:\Users\John\async-demo

04/25/2019  09:31 AM    <DIR>          .
04/25/2019  09:31 AM    <DIR>          ..
04/25/2019  02:32 PM             2,656 index.js
04/24/2019  08:07 AM               224 package.json
04/25/2019  10:10 AM             1,251 promise-api.js
04/25/2019  11:46 AM               673 promise.js
               4 File(s)          4,804 bytes
               2 Dir(s)  583,064,768,512 bytes free

C:\Users\John\async-demo>type package.json
{
  "name": "async-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

If you have comments or questions regarding this or any other post in this blog, or if you would like me to help with any phase in the SDLC (Software Development Life Cycle) of a product or service, please do not hesitate and leave me a note below. Requests for help will remain private.

Keep on reading and experimenting. It is the best way to learn!

John

Follow me on Twitter:  @john_canessa

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.