Promises and Lies

20 May 2020

Promises in Javascript

Promises were added to Javascript as a standard means to avoid nesting callback functions. Without promises, each function could have its own functions to be called once the job is complete. They are mostly used for asynchronous events. They are not a means of lazy evaluation or for process control. Functions that have an async result typically now return a Promise.

Promises have the following format:

const promise = new Promise(executor);

const promise = newPromise(executor);

where executor is a function that looks like:

function(resolve, reject){
    // typically, some asynchronous operation.
}

Where resolve and reject are functions that take the result as a single parameter.

Promises are used like this:

promise.then(console.log('It worked'))
       .catch(console.log('It failed'))
       .finally( console.log('It is over') )

It is possible to return a promise from a promise so that they can be chained. Note that the catch at the end of a chain will catch whichever reject happens first.

Once a promise has resolved to a value, then it will always resolve to the same value. You can, therefore, call then on a given promise multiple times and it will always return the same result. I'll come back to this concept later.

Promises also have no means of being cancelled. The only output of them is through actions of the resolve or reject functions. This means that it is possible to cache a promise - just note that the cached promise could have already failed!

The Promise class

The javascript promise class has the following static functions:

Promise.all( )

This will turn iterable of Promises into a Promise that will either return an iterable of the success values or the first failure condition.

Promise.allSettled()

This will take an iterable of Promises and returns a Promise that will return an iterable of the results (either resolved or rejected).

Promise.race()

This will take an iterable of Promises and returns a Promise that will return the result of the first resolved or rejected Promise.

Promise.reject()

This returns a Promise that has been rejected with the given reason.

Promise.resolve()

This will either return the result of a supplied promise or will return a Promise that has resolved the supplied data.

What is missing?

The standard library has no built-in concept of timeout. This means that each user must consider (or more likely ignore) this important concept. Without a controlled timeout you have no means of ensuring that a given then() will ever return.

Serverless functions are one example of where a fixed timeout is essential. The serverless framework adds a timeout value that defaults to 6 seconds. It is important that you get to choose how you handle failure rather than just having the function fail. More importantly, you are charged on execution time so waiting for a function to return costs money.

Introducing lie.js

This is where lie.js comes in. It provides a standard means of adding timeouts to Promises. These are very small functions that bring in no additional dependencies.

It currently provides two helper functions that are intended to be used in a Promise.race with an existing promise.

orReject

Promise.race([yourPromise, orReject('bang', 1)])

This will reject the promise with the given value after the timeout.

orTimeout

Promise.race([yourPromise, orTimeout('bang', 1)])

This will succeed the promise with the given value after the timeout.

These two simple functions allow you to add timeout functionality to any Javascript promise.

Promises do not have a cancel option!

I did promise that I would come back to the multiple evaluation point. Just because a promise in a race has been timed out does not mean that you can't try to get the result later.

import {orTimeout, orReject} from  'lie.js'

const timeoutPromise = orTimeout('bang', 1);
const rejectPromise = orReject('fail', 2);

Promise.race([timeoutPromise, rejectPromise])
.then( result => {
  console.log(result);
  rejectPromise.then( () => {}, x => console.log('second', x) );
 });

 rejectPromise.then( () => {}, x => console.log('first', x) );

This will output:

bang
first fail
second fail

This means that you can break a slow process down into chunks where you get the option to do something else such as call an alternative function, log or update the UI.

Consider using lie.js when you next need to use a Promise. Controlling the way that your functions fail is the only way to create reliable systems. Having a standard means of controlling timeout behaviour is very important.