Promises In JavaScript

Ben Aston
8 min readApr 10, 2017

--

Promises were invented in the mid-to-late 1970s at Indiana University in the US. They are a language construct used for synchronizing execution in concurrent programming environments.

Originally they were designed to decouple variable values from the time and place of their calculation, making parallelisation easier. Later, they were re-discovered for their ability to simplify asynchronous code by moving it to a more direct style rather than the continuation passing style necessitated without them.

Promises also make dealing with errors significantly easier.

JavaScript makes extensive use of asynchronous code because of the blocking nature of its single-threaded event loop implementation.

Understanding promises is therefore important for anyone who wants to understand JavaScript.

Continuation-Passing Style

This is how asynchronous JavaScript was written until the re-discovery of promises by the JavaScript community in the mid 2000s.

function foo(arg1, done) {
setTimeout(() => { // Simulate some async calculation
done(arg1 + 1);
});
}
function bar(arg1) {
console.log(arg1); // 2
}
foo(1, bar);

Consider how you would implement error handling in this simple example, bearing in mind `foo` is asynchronous (hint: it would be error-prone and take a bit of thought).

Direct Style

function foo(arg1, done) {
return new Promise(resolve => setTimeout(() => resolve(arg1 + 1));
}
function bar(arg1) {
console.log(arg1); // 2
}
function fail(err) {
console.log(err);
}
foo(1)
.then(bar)
.catch(fail);

Note how promises simplify error handling and how the API to the asynchronous function `foo` becomes easier to follow with `then` and `catch`.

The Built-In Promise Object

ES2015 includes the built-in `Promise` function-object. IE11 does not support this object, so you should use a polyfill like es6-promise.

Some libraries and languages make distinctions between futures, deferreds and promises. TC-39 (the standardization committee for JavaScript) deliberately chose to avoid this complexity and ES2015 does not distinguish between these kinds of construct.

The use of deferreds in JavaScript is therefore not recommended (or indeed necessary). Please note, however, that some libraries created prior to the addition of `Promise` in ES2015 do, for historical reasons, use the deferred construct.

A promise is effectively an API wrapping a state machine controlling access to a future value. The states of the state machine are pending, fulfilled and rejected. You can only move from pending to fulfilled or rejected, not the other way.

Resolved is often used to mean fulfilled, but the two are not exactly the same. A resolved promise can be:

  • resolved with a value which leads to fulfillment or,
  • resolved with a rejected promise or,
  • resolved with a pending promise

A resolved pending state means it is waiting on the eventual outcome of some other promise.

Example

The following creates a new promise that is resolved with the string `foo` after around five seconds.

new Promise(resolve => {
setTimeout(() => resolve('foo'), 5000);
});

API

The API for promise objects in ES2015 has a constructor and four important methods: `then`, `catch`, `all` and `race`.

Some promise libraries implement `cancel` for cancellable promises. ES2015 does not have this and although it was on TC-39’s radar for future inclusion, it now looks like it may have been killed. That said, cancellable promises can currently be implemented without too much hassle (see the example at the bottom of this post).

Constructor

The `Promise` constructor function is used to “promisify” functions. It accepts accepts a single function. This function is called the executor function and is evaluated synchronously and immediately.

So

new Promise(resolve => {
console.log('executor');
resolve();
});
console.log('line after promise');

…outputs (note the order)

executor
line after promise

The executor function is supplied with two callback arguments: `resolve` and `reject` in that order. These can be called to either resolve or reject the promise.

There are also shorthands for resolving and rejecting a promise:

Promise.resolve()

and

Promise.reject()

These shorthands can be supplied values to be propagated along the promise chain.

Promise.resolve('foo')
.then(result => console.log(result)); // Prints 'foo'

then

This method enables the chaining of asynchronous actions, enabling the direct coding style mentioned at the top of this article.

`then` accepts two optional callbacks `onFulfilled` and `onRejected`. The latter is used less frequently.

`onFulfilled` is invoked with the fulfilment value of the promise `then` was called on.

`onRejected` is called with the rejection reason of the promise `then` was called on.

`then` will schedule whichever of the callbacks you supply that needs to be run, as a microtask on the job queue. This is specified in the HTML5 specification and although not explicitly specified in other runtimes like Node.js, can probably be assumed to work the same.

This means that the callback(s) supplied to `then` will always be run asynchronously.

Microtasks are guaranteed to be run before the next macrotask.

This means that the `then` callback will be run before the next macrotask on the job queue, but after any synchronous code.

So

setTimeout(() => console.log('setTimeout1'));
new Promise(resolve => {
console.log('executor function');
setTimeout(() => console.log('setTimeout2'))
resolve('result');
})
.then(result => console.log(result));
console.log(‘line after promise chain’);

…outputs

executor function
line after promise chain
result
setTimeout1
setTimeout2

Importantly, if a function supplied to a `then` returns a promise, then the chain will wait for that promise to resolve or reject.

catch

This method enables convenient error handling. It accepts a single callback that will be called if an exception occurs inside or a promise chain is rejected. It has a similar asynchronous nature to `then`. The callback is supplied with an argument corresponding to the exception thrown or the value supplied to a call to `reject`.

setTimeout(() => console.log('setTimeout1'))
new Promise((resolve, reject) => {
console.log('executor function');
setTimeout(() => console.log('setTimeout2'))
reject('error');
})
.catch(error => console.log(error));
console.log('line after promise chain');

…outputs:

executor function
line after promise chain
error
setTimeout1
setTimeout2

Remember that `catch` will stop an error from propagating unless the error is explicitly re-thrown.

Accidental swallowing of errors is a common problem with promise-based code. Always remember to consider how errors will be swallowed or propagated in your promise chains.

new Promise((resolve, reject) => {
throw 'an error occurred'
})
.catch(error => console.log(error))
.then(() => console.log('we are good (as far as I know!)'));

…outputs:

an error occurred
we are good (as far as I know!)

Interaction With Synchronous Exception Handlers

Note that exceptions raised within the chain are not visible outside of the chain …which, given the async nature of the construct, should make sense.

try {
new Promise((resolve, reject) => {
throw 'an error occurred'
})
.catch(error => {
console.log(error);
throw error;
})
.then(() => console.log('we are good as far as I know!'));
} catch (err) {
console.log('error caught:', err); // never called here!
}

…outputs (note that “error caught” is not output):

an error occurred

Note also that an asynchronous function has two APIs — a synchronous exception API and an asynchronous promise-based-api.

If you do not need to use the `Promise` contstructor to promisify a function you can leverage the aforementioned `Promise.resolve` to avoid having to cater for synchronous exceptions and give your function a consistent, promise-based API.

return Promise.resolve()
.then(() => {
/* put your code in here */
})
.catch(error => {
console.log(error);
throw error;
});

…now all your exceptions will end up in the promise chain.

all

The `all` function lets all of the supplied asynchronous functions run in parallel. `all` will only resolve when all (duh!) the functions supplied in an array (or other iterable) resolve. It will reject as soon as one of the supplied functions rejects or throws an exception.

The following will reject the outer promise chain immediately and print `error`.

Note that the inner promise chain continues and `foo` is then printed to the console, although because the outer chain is by that time rejected, the result is not propagated.

var p1 = new Promise(resolve => setTimeout(() => resolve('foo')))
.then(result => console.log(result));
var p2 = Promise.reject('error');
Promise.all([p1, p2])
.then(() => console.log('all done'))
.catch(error => console.log(error));

race

`race` will resolve with the first resolved result of an array (or other iterable) of promises. Even if a subsequent promise is rejected. This can be used for things like handling network timeouts.

The following will print “all done” and the rejection will be ignored.

var work = new Promise(resolve => setTimeout(() => resolve('foo'), 100))
var timeout = new Promise((_, reject) => setTimeout(() => reject('a timeout occurred'), 200))
Promise.race([work, timeout])
.then(() => console.log('all done'))
.catch(error => console.log(error));

The following (as above but with timings inverted) will print “a timeout occurred” and the resolved promise will be ignored.

var work = new Promise(resolve => setTimeout(() => resolve('foo'), 200))
var timeout = new Promise((_, reject) => setTimeout(() => reject('a timeout occurred'), 100))
Promise.race([work, timeout])
.then(() => console.log('all done'))
.catch(error => console.log(error));

Usage Notes

When working with promises you should almost always return them.

Avoid code like this:

function doSomethingAsync() {
return new Promise(resolve => /* whatever */);
}
function foo() {
doSomethingAsync(); // You should return the promise!
}
foo();

By not returning the promise, `foo` prevents client code from controlling, augmenting or responding to the promise chain.

Furthermore you give client code an unavoidable race condition that they cannot control. This makes reasoning about the outcome or sequencing of events difficult or impossible and can lead to subtle bugs.

Sometimes, usually at the bootstrap of a component or application, swallowing promises like this is unavoidable. But this is the exception rather than the rule.

You should very nearly NEVER expose the `resolve` and `reject` callbacks outside the executor function.

Exposing `resolve` and `reject` makes testing, understanding and maintaining the code much MUCH more difficult. Do not do this.

Cancellable Promise Implementation

Even though JavaScript does not expose a built-in mechanism to cancel a promise, we can write our own.

const CANCELLED = Symbol('cancelled');function cancellable(promise) {
let isCancelled = false;
const wrapper = new Promise((resolve, reject) => {
promise.then(val => isCancelled ? reject(CANCELLED) : resolve(val));
promise.catch(error => isCancelled ? reject(CANCELLED) : reject(error));
});
return {
promise: wrapper,
cancel() {
isCancelled = true;
},
};
}
cancellable.CANCELLED = CANCELLED;
export default cancellable;

Generator Functions and yield

ES2015 brings with it generator functions and the contextual `yield` keyword.

These enable co-routine-like suspend and resume of function evaluation.

If we can suspend and resume mid-function we can construct a promise chain using code that is a straightforward sequence of statements with fewer callbacks. This might help reduce complexity and improve readability.

In order to achieve this we need to wrap our code in a generator function (let’s call it `asynch`) so we can take advantage of the `yield` contextual keyword and return/resume mid-function.

For example:

asynch(function*() {
const result1 = yield anAsyncFunction();
const result2 = yield anotherAsyncFunction();
console.log(result1 + result2);
});

…and the function `asynch` might look something like this:

function asynch(gFn) {
var g = gFn();
return Promise.resolve().then(go); function go() {
var result = g.next(…arguments);
if (isPromise(result.value)) {
return result.value.then(go);
}
if (Array.isArray(result.value)) {
return Promise.all(result.value).then(go);
}
if (isObject(result.value)) {
var o = {};
var promises =
Object.keys(result.value)
.map(k=>result.value[k].then(r=>o[k] = r));
return Promise.all(promises).then(()=>o).then(go);
}
return Promise.resolve(result);
}
}

The possibilities for this pattern were recognised by the community and by TC-39. A standardized implementation of the pattern is scheduled to be included in a future version of the language with two new keywords: `async` (effectively does what `asynch` does above) and `await` (similar to the use of `yield` above).

My name is Ben Aston and thank you for reading my post today. I am a London-based JavaScript consultant, available for short-term consultancy globally. I can be contacted at ben@bj.ma.

If you enjoyed this piece, please let me know by clicking the “recommend” button below.

You may also be interested in my post on closures in JavaScript.

Made in the United Kingdom

--

--