JS Promises

The woods are lovely, dark and deep. But I have promises to keep, and miles to go before I sleep. — Robert Frost

 p = new Promise((resolve, reject) => {
  resolve(10);
  resolve(20);
}).then(x => console.log(x));
p = new Promise((resolve, reject) => {
  reject(10);
  reject(20);
}).catch(x => console.log(x));
 p = new Promise((resolve, reject) => {
  resolve(10);
  reject(20);
}).then(x => console.log(x))
  .catch(x => console.log(x));

Output?

Promise.resolve(10)
  .then(x => console.log(x))
  .then(x => console.log(x))
  .then(x => console.log(x));
p = Promise.resolve(10);
p.then(x => console.log(x));
p.then(x => console.log(x));
p.then(x => console.log(x));
Promise.resolve(10)
  .then(x => {
    console.log(x);
    return 20;
  })
  .catch(x => {
    console.log(x);
    return 30;
  })
  .then(x => {
    console.log(x);
    return 40;
  })
  .then(x => {
    console.log(x);
  });
Promise.reject(10)
  .catch(x => {
    console.log(x);
    return 20;
  })
  .then(x => {
    console.log(x);
    return 30;
  })
  .catch(x => {
    console.log(x);
    return 40;
  })
  .then(x => { console.log(x); });

A Primer

  • Javascript is single-threaded
  • Code is generally synchronous
  • Javascript interprets each line right away before moving on to what’s next
  • I/O Code is expected to be non-blocking
console.log('start');
setTimeout(() => {
  console.log('middle');
}, 0);
console.log('end');
console.log('start');
fs.readFile('a.js', (err, data) => {
  console.log(`data - ${data}`);
});
console.log('end');

Concurrency is a way to structure a program by breaking it into pieces that can be executed independently

String fileName = "/Users/folder/source.txt";
File file = new File(fileName);
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
String line;
while((line = br.readLine()) != null){
    //process the line
    System.out.println(line);
}

An Example

Imagine a hypothetical scenario (where all operations are async) —

  • We query our database for the user Arfat. We read the profile_img_url and fetch the image from someServer.com.
  • After fetching the image, we transform it into a different format, say PNG to JPEG.
  • If the transformation is successful, we send the user an email.
  • We log this task in our file transformations.log with the timestamp.

Blocking I/O

class Example {
  public static void main(String[] args) {
    print('start');
    ResultSet rs = st.executeQuery(query);
    while (rs.next()) {          
      // result
    }

    URL url = new URL("http://www.someserver.com/");
    URLConnection connection = url.openConnection();
    while ((inputLine = in.readLine()) != null) 
              response.append(inputLine);
    
    PNG png = transform(picture);

    if (png != null) {
      sendEmail(user);
    } 
    log('Task')

    print('end');
  }
}

Using Callbacks

  • By default, async in JS is all about callbacks.
  • Callbacks get an otherwise blocking long-lived function out of the loop. They’re the exception to sequential, ‘blocking’ execution.
  • Callbacks don’t return values, they have *side-effects*
queryDatabase({ user: 'arfat' }, (err, user) => {

});

Using Callbacks

  • By default, async in JS is all about callbacks.
  • Callbacks get an otherwise blocking long-lived function out of the loop. They’re the exception to sequential, ‘blocking’ execution.
  • Callbacks don’t return values, they have *side-effects*
queryDatabase({ user: 'arfat' }, (err, user) => {
  const image_url = user.profile_img_url;
  getPicture(`someServer.com/q=${image_url}`, (err, image) => {
    
  });
});

Using Callbacks

  • By default, async in JS is all about callbacks.
  • Callbacks get an otherwise blocking long-lived function out of the loop. They’re the exception to sequential, ‘blocking’ execution.
  • Callbacks don’t return values, they have *side-effects*
queryDatabase({ user: 'arfat' }, (err, user) => {
  const image_url = user.profile_img_url;
  getPicture(`someServer.com/q=${image_url}`, (err, image) => {
    transform(picure, (err, png) => {
      
    })
  });
});

Using Callbacks

  • By default, async in JS is all about callbacks.
  • Callbacks get an otherwise blocking long-lived function out of the loop. They’re the exception to sequential, ‘blocking’ execution.
  • Callbacks don’t return values, they have *side-effects*
queryDatabase({ user: 'arfat' }, (err, user) => {
  const image_url = user.profile_img_url;
  getPicture(`someServer.com/q=${image_url}`, (err, image) => {
    transform(picure, (err, png) => {
      if (err) {
        // handle errors
      } else {
        sendEmail(user, (err, success) => {
          log(success);
        });
      }
    })
  });
});

Using Callbacks

  • By default, async in JS is all about callbacks.
  • Callbacks get an otherwise blocking long-lived function out of the loop. They’re the exception to sequential, ‘blocking’ execution.
  • Callbacks don’t return values, they have *side-effects*
queryDatabase({ user: 'arfat' }, (err, user) => {
  const image_url = user.profile_img_url;
  getPicture(`someServer.com/q=${image_url}`, (err, image) => {
    transform(picure, (err, png) => {
      if (err) {
        // handle errors
      } else {
        sendEmail(user, (err, success) => {
          log(success);
        });
      }
    })
  });
});

Disadvantages of Callbacks

  • Readability
    • Pyramid of Doom / Callback Hell
  • Complexity
    • Non-sequential exeution
  • Debugging
    • The callback is executed much later so the stack trace is different.
  • Loss of control flow
    • return does not return to the parent function
  • Loss of error handling
    • Exception won't be caught by the parent function
  • Sync/Async Ambiguity
  • Harder to compose and modularization
    • No Chaining

Loss of Error Handling

try {
  download(url, function(image) {
    image.show();
  });
} catch (e) {
  // never executed!! console.log("Cannot show image")
}
try {
  // is X Asynchronous?!?!
  X(url, function(image) {
    image.show();
  });
} catch (e) {
  // never executed!! console.log("Cannot show image")
}

Sync/Async Ambiguity

 p = new Promise((resolve, reject) => {
  resolve(10);
  resolve(20);
}).then(x => console.log(x));
p = new Promise((resolve, reject) => {
  reject(10);
  reject(20);
}).catch(x => console.log(x));
 p = new Promise((resolve, reject) => {
  resolve(10);
  reject(20);
}).then(x => console.log(x))
  .catch(x => console.log(x));

Output?

Promises (aka futures, deferred)

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.

An object which represents and manage the lifecycle of a future result

The Promise API

const p = new Promise()

Promises (aka futures, deferred)

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.

An object which represents and manage the lifecycle of a future result

The Promise API

const p = new Promise((resolve, reject) => {
  
})

Promises (aka futures, deferred)

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.

An object which represents and manage the lifecycle of a future result

The Promise API

const p = new Promise((resolve, reject) => {
  setTimeout(resolve, 2000);
})

Promises (aka futures, deferred)

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.

An object which represents and manage the lifecycle of a future result

The Promise API

const p = new Promise((resolve, reject) => {
  setTimeout(resolve, 2000);
});

p.then(() => console.log('after 2 seconds'));

Promises (aka futures, deferred)

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.

An object which represents and manage the lifecycle of a future result

The Promise API

const wait = (ms) => new Promise((resolve, reject) => {
  setTimeout(resolve, ms);
});

wait(2000).then(() => console.log('after 2 seconds'));

Promise States

  • A Promise is always in one of three mutually exclusive states:
    • Before the result is ready, the Promise is pending.
    • If a result is available, the Promise is fulfilled.
    • If an error happened, the Promise is rejected.
  • A Promise is settled if “things are done” (if it is either fulfilled or rejected).
  • A Promise is settled exactly once and then remains unchanged.

A thenable is an object that has a Promise-style then() method.

Consuming Promises

A Promise object serves as a link between the executor and the consuming functions, which will receive the result or error.

Consumers can be registered using one of the following methods

  • .then
  • .catch
  • .finally

Promise.prototype.then

 Promise.prototype.then(undefined, onRejected)

  • Takes two function
    • onFulfilled
    • onRejected
  • The .then is executed only after a promise is resolved.
  • The .then always returns a promise.
  • You can chain the multiple .thens.
  • Promise.prototype.catch is syntactic sugar for Promise.prototype.then(undefined, onRejected)

Promise.prototype.catch

  • If the promise is rejected, the next nearest .catch is called.
  • The .catch itself returns a fulfilled promise by default, so you can continue chaining.

Promise.prototype.finally

  • When the promise is settled, i.e either fulfilled or rejected, the specified callback function is executed.
  • The finally() method is very similar to calling .then(onFinally, onFinally).
  • A finally callback will not receive any argument.

You can attach handlers to settled promises later

The consumer callbacks do not have to be attached at the moment of Promise construction.

let promise = new Promise((resolve) =>
  setTimeout(() => resolve('done!'), 1000)
).then(console.log);
let promise = new Promise((resolve) =>
  setTimeout(() => resolve('done!'), 1000)
);

promise.then(console.log);

They can be attached later.

Solutions

 p = new Promise((resolve, reject) => {
  resolve(10);
  resolve(20);
}).then(x => console.log(x));
// 10
p = new Promise((resolve, reject) => {
  reject(10);
  reject(20);
}).catch(x => console.log(x));
// 10
 p = new Promise((resolve, reject) => {
  resolve(10);
  reject(20);
}).then(x => console.log(x))
  .catch(x => console.log(x));
// 10
Promise.resolve(10)
  .then(x => console.log(x))
  .then(x => console.log(x))
  .then(x => console.log(x));
p = Promise.resolve(10);

p.then(x => console.log(x));
p.then(x => console.log(x));
p.then(x => console.log(x));

Output?

Chaining of Promises

Chaining of promises mean that the result of one promise will be fed to the next promise.

Very analogous  to the Unix Pipe operator

asyncFunc1()
.then(result1 => {
    // Use result1
    // A
    return asyncFunction2();
})
.then(result2 => {
    // Use result2
    // B
})
.catch(error => {
    // Handle errors of 
    // asyncFunc1() and asyncFunc2()
});

How the Promise returned by then() is settled depends on what its callback does:

If it returns a Promise (as in line A), the settlement of that Promise is forwarded to P. That’s why the callback from line B can pick up the settlement of asyncFunction2’s Promise.

If it returns a different value, that value is passed.

If it throws an exception then P is rejected with that exception.

Attaching multiple handlers to the same promise

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);
});

promise.then(function(result) {
  return result * 2;
});

promise.then(function(result) {
  return result * 2;
});

promise.then(function(result) {
  return result * 2;
});
Promise.resolve(10)
  .then(x => console.log(x))
  .then(x => console.log(x))
  .then(x => console.log(x));
// 10 undefined undefined
p = Promise.resolve(10);
p.then(x => console.log(x));
p.then(x => console.log(x));
p.then(x => console.log(x));
// 10 10 10

Solutions

Error handling with promises

  • When a promise rejects, the control jumps to the closest rejection handler.
  • The execution can continue from that point.

A promise can reject by

  • by calling the reject function
  • by throwing inside a then handler or executor function
Promise.reject(10)
  .catch(x => {
    console.log(x);
    return 20;
  })
  .then(x => {
    console.log(x);
    return 30;
  })
  .catch(x => {
    console.log(x);
    return 40;
  })
  .then(x => { console.log(x); });

Example

unhandledRejections

window.addEventListener('unhandledrejection', function(event) {
  // the event object has two special properties:
  // [object Promise] - the promise that generated the error
  alert(event.promise); 
  // Error: Whoops! - the unhandled error object
  alert(event.reason); 
});

// Node
process.on('unhandledRejection', (error, promise) => {
  console.log(error, promise);
});

new Promise(function() {
  throw new Error("Whoops!");
}); // no catch to handle the error
Promise.resolve(10)
  .then(x => {
    console.log(x);
    return 20;
  })
  .catch(x => {
    console.log(x);
    return 30;
  })
  .then(x => {
    console.log(x);
    return 40;
  })
  .then(x => {
    console.log(x);
  });

Test yourself!

const promise = new Promise((resolve, reject) => {
  // perform action, call resolve 
  // or reject upon completion / error
});
promise.then(result => {
  // listen for completion
});
promise.then(
  result => {} // listen for completion 
  (error) => {} // listen for error
);
promise.catch(error => {
  // listen for error
});
promise.finally(() => {
  // cleanup
});

Summary

Promise.reject(10)
  .catch(x => {
    console.log(x);
    return 20;
  })
  .then(x => {
    console.log(x);
    return 30;
  })
  .catch(x => {
    console.log(x);
    return 40;
  })
  .then(x => { console.log(x); });
// 10 20 30
Promise.resolve(10)
  .then(x => {
    console.log(x);
    return 20;
  })
  .catch(x => {
    console.log(x);
    return 30;
  })
  .then(x => {
    console.log(x);
    return 40;
  })
  .then(x => {
    console.log(x);
  });
// 10 20 40
const wait = (ms) => new Promise((resolve, reject) => {
  const randomNumber = Math.random();
  if (randomNumber > 0.5) {
    setTimeout(() => reject(`Rej-${randomNumber}`), ms);
  } else {
    setTimeout(() => resolve(`Res-${randomNumber}`), ms);
  }
});

wait(2000)
  .then(console.log)
  .catch(console.log)
  .finally((...args) => console.log('done', args.length));

Promise Static Methods (API)

Promise Construction

  • Promise.resolve
  • Promise.reject

Aggregate Operations on Promises

  • Promise.all [ES2015]
  • Promise.race [ES2015]
  • Promise.allSettled [ES2020]
  • Promise.any [ES2021]

Promise.resolve

  • For most values x, it returns a Promise that is fulfilled with x:
Promise.resolve('abc')
    .then(x => console.log(x)); // abc
  • If x is a Promise then x is returned unchanged:
const p = new Promise(() => null);
console.log(Promise.resolve(p) === p); // true

There are 3 major semantic behaviors of resolve

  • If x is a thenable, it is converted to a Promise: the settlement of the thenable will also become the settlement of the Promise.
const fulfilledThenable = {
  then(resolve, reject) {
    resolve('hello');
    console.log(typeof reject);
  },
};
const promise = Promise.resolve(fulfilledThenable);
console.log(promise instanceof Promise); // true
promise.then((x) => console.log(x)); // hello

The idea is that 3rd-party libraries may implement “promise-compatible” objects of their own.

Promise.reject

The static Promise.reject function returns a Promise that is rejected.

Promise.reject(new Error('fail'))
  .then(function() {
  	// not called
  }, function(error) {
    console.error(error);
  });
Promise.resolve(10)
  .then(x => {
    console.log(x);
    return 20;
  })
  .then(x => {
    console.log(x);
    throw new Error(30);
  })
  .then(x => {
    console.log(x);
    return 40;
  })
  .catch(x => {
    console.log(x);
    return 50;
  });
Promise.reject(10)
  .catch(x => console.log(x))
  .catch(x => console.log(x))
  .catch(x => console.log(x));
const p = new Promise(res => {
  res(10);
});
Promise.reject(p)
  .catch((x) => {
    console.log('rejected with ', x);
  });

Promise.resolve(10)
  .then(x => {
    console.log(x);
    return 20;
  })
  .then(x => {
    console.log(x);
    throw new Error(30);
  })
  .then(x => {
    console.log(x);
    return 40;
  })
  .catch(x => {
    console.log(x);
    return 50;
  });
// 10 20 Error(30)
Promise.reject(10)
  .catch(x => console.log(x))
  .catch(x => console.log(x))
  .catch(x => console.log(x));

// 10
const p = new Promise(res => {
  res(10);
});
Promise.reject(p)
  .catch((x) => {
    console.log('rejected with ', x);
  });
// rejected with  Promise { 10 }

Promise Static Methods (API)

Promise Construction

  • Promise.resolve
  • Promise.reject

Aggregate Operations on Promises

  • Promise.all [ES2015]
  • Promise.race [ES2015]
  • Promise.allSettled [ES2020]
  • Promise.any [ES2021]

Promise.all

  • An already resolved Promise if the iterable passed is empty.
  • An pending promise when the iterable contains a promise.
Promise.all([
    asyncFunc1(),
    asyncFunc2(),
])
.then(([result1, result2]) => {
    ···
})
.catch(err => {
    // Receives first rejection among the Promises
    ···
});

all lets you know when either all input promises have fulfilled or when one of them rejects.

Promise.race

  • It takes an iterable over Promises (thenables and other values are converted to Promises via Promise.resolve()) and returns a Promise P.
  • The first of the input Promises that is settled passes its settlement on to the output Promise.
  • If iterable is empty then the Promise returned by race() is never settled.
Promise.race([
    httpGet('http://example.com/file.txt'),
    delay(5000).then(function () {
        throw new Error('Timed out')
    });
])
.then(function (text) { ··· })
.catch(function (reason) { ··· });

Promise.allSettled

The Promise.allSettled() method returns a promise that resolves after all of the given promises have either settled, with an array of objects that each describes the outcome of each promise.

Promise.allSettled([
  Promise.resolve('a'),
  Promise.reject('b'),
])
.then(arr => assert.deepEqual(arr, [
  { status: 'fulfilled', value:  'a' },
  { status: 'rejected',  reason: 'b' },
]));

Promise.any

  • Promise.any gives you a signal as soon as one of the promises fulfills.
  • This is similar to Promise.race, except any doesn’t reject early when one of the promises rejects.
const pErr = new Promise((resolve, reject) => {
  reject('Always fails');
});

Promise.any([pErr]).catch((err) => {
  console.log(err.errors);
});
name description
Promise.allSettled does not short-circuit
Promise.all short-circuits when an input value is rejected
Promise.race short-circuits when an input value is settled
Promise.any short-circuits when an input value is fulfilled

Summary

deck

By Arfat Salman