JavaScript Promises

Dr. Greg Bernstein

Updated March 18th, 2021

Promises

Learning Objectives

  • Understand the fundamentals of Promises in JavaScript
  • Learn how to chain together Promises to control the order of asynchronous processing
  • Compose Promises for even more control over processing order

Readings/References

Asynchronous Programming in JS

  • event queue and callback functions

  • Promises refines the above in a standard way with a number of benefits. They are incorporated in a number of standard APIs.

  • Async/Await Functions in ES2017 build on Promises

General Definition

From Promises/A+

A promise represents the eventual result of an asynchronous operation. The primary way of interacting with a promise is through its then method, which registers callbacks to receive either a promise’s eventual value or the reason why the promise cannot be fulfilled.

Constructing a promise

  • new Promise(function(resolve, reject) { ... } );
  • Where the “executor function” (a callback) takes two arguments
    • resolve: a callback function that is called if the promise resolves successfully.
    • reject: a callback function that is called if the promise is rejected.

A promise has state

In fact can be in three states:

  • pending: initial state, not fulfilled or rejected.
  • fulfilled : meaning that the operation completed successfully.
  • rejected : meaning that the operation failed.

Simplest Promise

SimplestPromise.js: Try on the console or with Node.js.

myP = new Promise(function(resolve, reject){ // Trivial promise
    resolve("Hi Web Systems!");
});

function sucessHandler(msg) { // If things go well
    console.log(msg);
}

function rejectHandler() { // If things don't go well
    console.log("It was rejected!");
}

myP.then(sucessHandler, rejectHandler); // See what happens...

More on .then()

  • p.then(onFulfilled[, onRejected]);

  • onFulfilled: A Function called when the Promise is fulfilled. This function has one argument, the fulfillment value.

  • onRejected (Optional): A Function called when the Promise is rejected. This function has one argument, the rejection reason.

A little more interesting

Wait for it… timePromise.js

myP = new Promise(function(resolve, reject){ // Trivial promise
    setTimeout(()=>resolve("Hi Web Systems!"), 5000);
});

function sucessHandler(msg) { // If things go well
    console.log(msg);
}

myP.then(sucessHandler);
console.log("I was called after myP.then ...");

Checking out the reject path

rejectPromise.js: Try on the console or with Node.js.

myP = new Promise(function(resolve, reject){ // Trivial promise
    setTimeout(()=>reject("Something bad happened!"), 2000);
});

function sucessHandler() { // If things go well
    console.log("Things are Great!");
}

function rejectHandler(msg) { // If things don't go well
    console.log(msg);
}

console.log("Trying and getting rejected!");
myP.then(sucessHandler, rejectHandler); // See what happens...

Multiple Listeners?

multipleListeners.js: Try on the console or with Node.js.

myP = new Promise(function(resolve, reject){
    setTimeout(()=>resolve("Hi Web Systems!"), 2000);
});

myP.then(function(msg) {console.log("listener 1: " + msg)})
myP.then(function(msg) {console.log("listener 2: " + msg)})

console.log("Called after myP.then ...");

Catch

An alternative for listening for rejection. From catch()

The catch() method returns a Promise and deals with rejected cases only. It behaves the same as calling Promise.prototype.then(undefined, onRejected).

Catch Example

myP = new Promise(function(resolve, reject){
    setTimeout(()=>reject("Something Bad :-<"), 2000);
});

myP.then(function(msg) {console.log("Doing Great!")})
.catch(function(msg) {console.log(msg)});
console.log("Called after myP.then and myP.catch ...");

Kind of like Events

  • We would register event listeners for specific types of events.
  • It seems like Promises are the same. Can we ever miss a promise if we are late to “check it”?

Late to the Party?

lateParty.js: Try checking the promise way after it should have been resolved:

myP = new Promise(function(resolve, reject){
    setTimeout(()=>resolve("Hi Websystems!"), 100); // 0.1 second!
});

// These will check on the promise much later...
setTimeout(() => myP.then(function(msg) {console.log("listener 1: " + msg)}), 3000);

setTimeout(() => myP.then(function(msg) {console.log("listener 2: " + msg)}), 6000);

Returning Modified Promises

We can modify promises and return them modifyPromise.js:

myP = new Promise(function(resolve, reject){
    setTimeout(()=>resolve("Hi Web Systems!"), 3000);
});

// What is myP2?
myP2 = myP.then(function(msg) {return "I saw: " + msg;});

console.log("Is myP2 a Promise? " + (myP2 instanceof Promise));

myP2.then(function(msg) {console.log(msg)});

Notes

  • We can listen to promises at multiple places.

  • We can never miss a promise

  • We can return modified promises (a form of process chaining)

Why Bother?

  • Web programming frequently involves significant time delays
  • Event based paradigms can lead to “callback hell”
  • Promises can help. Many modern JavaScript APIs return promises. The fetch API is one that we will be using.

Chaining

Chaining Examples

Let’s see how we can use promises to help us manage asynchronous execution

  • A Silly Clock (can do this in Node or a browser)
  • Requests with Promises

Silly Clock I

badTimes.js What will this do?

myTime = 0.0;
startTime = new Date();

function advanceTime() {
    myTime += 1.0;
    elapsedTime = (new Date() - startTime)/1000.0;
    console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
}
// What will this do?
setTimeout(advanceTime, 1000);
setTimeout(advanceTime, 1000);
setTimeout(advanceTime, 1000);
setTimeout(advanceTime, 1000);

Silly Clock I: Result

Not really a good clock by any measure…

$ node badTimes.js
myTime = 1, elapsedTime = 1.001
myTime = 2, elapsedTime = 1.003
myTime = 3, elapsedTime = 1.003
myTime = 4, elapsedTime = 1.004

Silly Clock II

goodTimes.js: Nest those calls!

myTime = 0.0;
startTime = new Date();
// What will this do?
setTimeout(function(){
        myTime += 1.0;
        elapsedTime = (new Date() - startTime)/1000.0;
        console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
        setTimeout(function(){
            myTime += 1.0;
            elapsedTime = (new Date() - startTime)/1000.0;
            console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
            setTimeout(function(){
                myTime += 1.0;
                elapsedTime = (new Date() - startTime)/1000.0;
                console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
                setTimeout(function(){
                    myTime += 1.0;
                    elapsedTime = (new Date() - startTime)/1000.0;
                    console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
                }, 1000);
            }, 1000);
        }, 1000);
    }, 1000);

Silly Clock II: Result

It counts! But not pretty

$ node goodTimes.js
myTime = 1, elapsedTime = 1.001
myTime = 2, elapsedTime = 2.005
myTime = 3, elapsedTime = 3.009
myTime = 4, elapsedTime = 4.01

Silly Clock IIIa: Promises

Let’s try throwing around a promise goodTimePromise.js

myTime = 0.0;
startTime = new Date();

function oneSecond() { // Returns a promise that resolves in one second
    return new Promise(function(resolve, reject){
        setTimeout(()=>resolve(), 1000);
        });
}

function advanceTime() {
    myTime += 1.0;
    elapsedTime = (new Date() - startTime)/1000.0;
    console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
    return oneSecond(); // Returns another new one second promise
}
// What will this do?
oneSecond()
    .then(advanceTime)
    .then(advanceTime)
    .then(advanceTime);

Silly Clock IIIb: Promises

Let’s try throwing around a promise goodTimePromise.js

myTime = 0.0;
startTime = new Date();

function oneSecond() { // Returns a promise that resolves in one second
    return new Promise(function(resolve, reject){
        setTimeout(()=>resolve(), 1000);
        });
}

function advanceTime() {
    myTime += 1.0;
    elapsedTime = (new Date() - startTime)/1000.0;
    console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
    return oneSecond(); // Returns another new one second promise
}
// What will this do?
let p1 = oneSecond();
let p2 = p1.then(advanceTime);
let p3 = p2.then(advanceTime);

Silly Clock III: Result

$ node goodTimePromise.js
myTime = 1, elapsedTime = 1.003
myTime = 2, elapsedTime = 2.01
myTime = 3, elapsedTime = 3.01

Promise Composition

Promise Composition I

From Promise.all():

The Promise.all() method returns a single Promise that resolves when all of the promises in the iterable argument have resolved, or rejects with the reason of the first promise that rejects.

Promise.all(iterable);

Example (browser)

allPromise.js

myP1 = new Promise(function(resolve, reject){
    setTimeout(()=>resolve("Hi from P1!"), 1000);
});
myP2 = new Promise(function(resolve, reject){
    setTimeout(()=>resolve("Hi from P2!"), 5000);
});
myP3 = new Promise(function(resolve, reject){
    setTimeout(()=>resolve("Hi from P3!"), 2000);
});
myPs = [myP1, myP2, myP3];
myP1.then((msg) => console.log(msg));
Promise.all(myPs).then((msg) => console.log(msg));

Promise Composition II

From MDN race

The Promise.race(iterable) method returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects, with the value or reason from that promise.

Racing Requests

Browser Example

myP1 = new Promise(function(resolve, reject){
    setTimeout(()=>resolve("P1"), 1000);
});
myP2 = new Promise(function(resolve, reject){
    setTimeout(()=>resolve("P2"), 5000);
});
myP3 = new Promise(function(resolve, reject){
    setTimeout(()=>resolve("P3"), 2000);
});
myPs = [myP1, myP2, myP3];
Promise.race(myPs).then((msg) => console.log(`the winner is ${msg}`));

Promises and Node.js APIs

Callback Based Node APIs

  • Node.js achieves good performance by using an event driven model for file system and network operations.

  • This requires the use of callback functions, e.g., see DNS and File System API documentation.

  • Wouldn’t it be nice to have a Promise based version of these API’s to simplify our code?

util.promisify(original)

From util.promisify(original) documentation

  • util.promisify(original)
  • original <Function>, Returns: <Function>

Takes a function following the common error-first callback style, i.e. taking an (err, value) => … callback as the last argument, and returns a version that returns promises.

Ugly Callback Based File Concatenation

nodeFileCat2.js

const fs = require('fs');
const dirRoot = __dirname + "/sample_files/";
let myString = " Empty\n";

// Nest callbacks to gurantee ordering
fs.readFile(dirRoot+"samp1.txt", 'utf8', function(err, data){
  if (err) throw err;
  myString = data + "\n";
  fs.readFile(dirRoot+"samp2.txt", 'utf8', function(err, data){
      if (err) throw err;
      myString += data + "\n";
      fs.readFile(dirRoot+"samp3.txt", 'utf8', function(err, data){
          if (err) throw err;
          myString += data + "\n";
          console.log(myString);  // Executes after all files have been read in order
        });
    });
});

Promise Based File Concatenation

nodeFileCatPromise.js

const fs = require("fs");
const util = require("util");
const dirRoot = __dirname + "/sample_files/";
let myString = " Empty\n";

const readFP = util.promisify(fs.readFile);
// Chaining Promises to guarantee ordering
readFP(dirRoot + "samp1.txt", "utf8")
    .then(function(data) {
        myString = data + "\n";
        return readFP(dirRoot + "samp2.txt", "utf8");
    })
    .then(function(data) {
        myString += data + "\n";
        return readFP(dirRoot + "samp3.txt", "utf8");
    })
    .then(function(data) {
        myString += data + "\n";
        console.log(myString);
    })
    .catch(function(err) {
        console.log("Some type of file error!");
    });

Promise Based File Concatenation with Async/Await

nodeFileCatAsync.js

const fs = require("fs");
const util = require("util");
const dirRoot = __dirname + "/sample_files/";
let myString = " Empty\n";
const readFP = util.promisify(fs.readFile);
// Using await to guarantee ordering.

async function fileCombine() {
    try {
    myString += await readFP(dirRoot + "samp1.txt", "utf8") + "\n";
    myString += await readFP(dirRoot + "samp2.txt", "utf8") + "\n";
    myString += await readFP(dirRoot + "samp3.txt", "utf8") + "\n";
    console.log(myString);
    } catch(e) {
        console.log("Some type of file error!");
    }
}
fileCombine();
// reveal.js plugins