JavaScript Promises

Dr. Greg Bernstein

September 12th, 2019

Promises

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 succesfully.
    • 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}`));

HTTP Requests and Promises

Node.js and Browser APIs

  • HTTP requests are subject to unpredictable and “relatively” long delays compared to local processing
  • To avoid blocking the request library for Node.js and the browser’s fetch API are asynchronous
  • The fetch API returns promises, the request API uses callbacks :-<

Request Library and Promises

We’ll use Request-Promise-Native

Installation:

npm install --save request
npm install --save request-promise-native

Documentation

Request-Promise-Native has some nice API improvements see:

Request-Promise

HTTP Detour Methods

Methods

Example 3 in order

threeInOrder.js

const rp = require('request-promise-native');
let site1 = {
    uri: 'https://www.grotto-networking.com',
    method: 'HEAD', // What does this do?
    resolveWithFullResponse: true
};

let site2 = {
    uri: 'http://www.google.com',
    method: 'HEAD',
    resolveWithFullResponse: true
};

let site3 = {
    uri: 'http://www.kiteboardingcairns.com.au/kitesurfing-australia/',
    method: 'HEAD',
    resolveWithFullResponse: true
};

let start = new Date();
rp(site1).then(res => {
    // console.log(`Grotto status: ${JSON.stringify(res)}`);
    let time = (new Date() - start)/1000;
    console.log(`Grotto status: ${res.statusCode}, time: ${time}`);
    return rp(site2);
}).then(res => {
    let time = (new Date() - start)/1000;
    console.log(`Google status: ${res.statusCode}, time: ${time}`);
    return rp(site3);
}).then(res => {
    let time = (new Date() - start)/1000;
    console.log(`Aus kiteboarding status: ${res.statusCode}, time: ${time}`);
})
console.log("Starting my web requests:");

Making Parallel Requests

Suppose we don’t care about the order in which the information comes back we just need to know when all our requests have been satisfied?

Example Node.js/Requests

threeInParallel.js

const rp = require('request-promise-native');
let site1 = {
    uri: 'https://www.grotto-networking.com',
    method: 'HEAD', // What does this do?
    resolveWithFullResponse: true
};

let site2 = {
    uri: 'http://www.google.com',
    method: 'HEAD',
    resolveWithFullResponse: true
};

let site3 = {
    uri: 'http://www.kiteboardingcairns.com.au/kitesurfing-australia/',
    method: 'HEAD',
    resolveWithFullResponse: true
};

let start = new Date();
let p1 = rp(site1).then(res => {
    // console.log(`Grotto status: ${JSON.stringify(res)}`);
    let time = (new Date() - start)/1000;
    return console.log(`Grotto status: ${res.statusCode}, time: ${time}`);});

let p2 = rp(site2).then(res => {
    let time = (new Date() - start)/1000;
    return console.log(`Google status: ${res.statusCode}, time: ${time}`);
});

let p3 = rp(site3).then(res => {
    let time = (new Date() - start)/1000;
    return console.log(`Aus kiteboarding status: ${res.statusCode}, time: ${time}`);
});

console.log("Starting my web requests:");
Promise.all([p1, p2, p3]).then(x=>{
    console.log("All Finished");
});

First Answer

Suppose we make a set of parallel requests but only want to wait till we get the first answer?

Racing Requests

threeInRace.js

const rp = require('request-promise-native');
let site1 = {
    uri: 'https://www.grotto-networking.com',
    method: 'HEAD', // What does this do?
    resolveWithFullResponse: true
};

let site2 = {
    uri: 'http://www.google.com',
    method: 'HEAD',
    resolveWithFullResponse: true
};

let site3 = {
    uri: 'http://www.kiteboardingcairns.com.au/kitesurfing-australia/',
    method: 'HEAD',
    resolveWithFullResponse: true
};

let start = new Date();
let p1 = rp(site1).then(res => {
    // console.log(`Grotto status: ${JSON.stringify(res)}`);
    let time = (new Date() - start)/1000;
    return console.log(`Grotto status: ${res.statusCode}, time: ${time}`);});

let p2 = rp(site2).then(res => {
    let time = (new Date() - start)/1000;
    return console.log(`Google status: ${res.statusCode}, time: ${time}`);
});

let p3 = rp(site3).then(res => {
    let time = (new Date() - start)/1000;
    return console.log(`Aus kiteboarding status: ${res.statusCode}, time: ${time}`);
});

console.log("Starting my web requests:");
Promise.race([p1, p2, p3]).then(x=>{
    console.log("was the winner! \n The rest: ");
});

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 Concat 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();