My first exposure to JavaScript Promises was at a JavaScript meetup a couple of years ago and despite being a talk by a very good presenter, it was an early stage in my JavaScript understanding and most of the topic went over my head. Nowadays Promises are quite common and most developers will probably have had some level of exposure, whether they understand them or not. They’re part of the EcmaScript 6 (ES6) specification so if you don’t understand them, now is a great time to learn.
We’re going to write an implementation of a JavaScript Promises library, and hopefully this helps you reading it and me writing it to better understand the workings of Promises. Let’s dive right into the example and a bit of an explanation of how Promises are used. For more information, check out Dr. Axel Rauschmayer’s article on ES6 Promises or Jake Archibald’s article on Javascript Promises.
Using Promises and the demo app
The basic usage of Promises is something like below, so that’s where we’re going to start. doSomething()
is going to do something asynchronous and then if it succeeds, we want to be able to use or manipulate the resulting value (if any) from doSomething()
in the success function. If something goes wrong in doSomething()
we want to be able to handle the error in the failure function.
doSomething().then(
function(value) {
// success
},
function(error) {
// failure
}
);
Take a look at script.js in the demonstration repository at branch step0
for how we’ll use the Promise library for the initial implementation.
button.addEventListener('click', function clickListener() {
setLoading(true);
fetch(imgUrl, { responseType: 'blob' }
.then(function(result) {
setLoading(false);
img.src = URL.createObjectURL(result);
}, function(error) {
setLoading(false);
console.error('Got no image!', error);
});
});
We’re fetching an image, then on success we stop the loading spinner and set the <img>.src
to an Object URL containing the loaded image data. On failure we still stop the loading spinner and log the error. We’ll improve on this and remove the duplication as we improve the Promise library.
Inside doSomething()
is where we’ll actually setup our Promise, start an asynchronous task and then ‘resolve’ (execute the success handler function) or ‘reject’ (execute the failure handler function) the Promise. A Promise can only be ‘settled’ (either ‘resolved’ or ‘rejected’) once. For the ES6 standard of Promises, we pass a function to the new Promise()
constructor. The function has two parameters resolve
and reject
which are both functions that we use to resolve or reject the Promise after our asynchronous task. In the example below, AsyncTask()
is a fictional utility that we can attach success and error handler functions to the onSuccess
and onError
properties.
function doSomething() {
return new Promise(function(resolve, reject) {
var task = new AsyncTask();
task.onSuccess = function(value) {
resolve(value);
};
task.onError = function(error) {
reject(error);
};
task.start();
});
}
Looking at fetch.js in the demonstration repository at branch step0
you’ll see how I’ve wrapped an XMLHttpRequest with an asynchronous fetch()
function that returns a Promise. I won’t explain this in any detail and it won’t be changing through this article, just take note of where we create the new Promise()
and where we resolve
and reject
the Promise.
The demonstration is just a placeholder image and a ‘Get Random Image!’ button. Clicking the button downloads a random 500x500 image from lorempixel.com and replaces the placeholder image with the downloaded image data.
To run the demonstration you’ll need to clone the git repository, git checkout
the correct branch (in this case step0
) and then serve the files using a HTTP server. If you have Python 2 or Python 3 installed, the easiest is to just run python -m SimpleHTTPServer
or python3 -m http.server
respectively from the command line inside the cloned git repository.
Basic success/failure handling
If you try running the demonstration now, you’ll get an error because promise.js is just an empty stub at the moment. To get the demonstration working, we’ll need to flesh out the constructor function and implement the .then()
method. The constructor just needs to initialise two internal properties to store the success and failure handler functions that are passed into .then()
, and then it needs to call the deferred function that is passed into the constructor. We’ll need to provide the resolve
and reject
methods that will be called from inside the deferred function that was passed into the constructor. Our initial, very basic implementation looks like this.
// promise.js
function Promise(deferred) {
this._success = null;
this._failure = null;
deferred(
function resolve(value) {
if (this._success) {
this._success(value);
}
}.bind(this),
function reject(error) {
if (this._failure) {
this._failure(error);
}
}.bind(this)
);
}
Promise.prototype.then = function(success, failure) {
if (success) {
this._success = success;
}
if (failure) {
this._failure = failure;
}
};
This will get the current demonstration application working. In the demo repository, git checkout step1
to see it working. At the moment, you can only place a single .then()
after the returned Promise from fetch()
, so we just log the error if you have no network connection or request a bad URL. Change fetch(imgUrl, { responseType: 'blob' });
to fetch(errorUrl, { responseType: 'blob' });
to demonstrate this. Don’t forget to change it back!
Handler chaining
One of the great strengths of the Promise pattern is being able to chain operations to happen after an asynchronous operation, and eventually be able to perform a number of asynchronous operations in a style that looks synchronous, or is at least easier for us to understand and reason with. We want to be able to do something like this.
method()
.then(/* ... */)
.then(/* ... */)
.catch(/* ... */)
.then(/* ... */);
Have a quick look at script.js on branch step2
to see how the demo has been changed. We don’t need to duplicate the setLoading(false)
calls anymore, we can do that at the end of the chain. We’ve separated some of the processing and notice the new .catch()
method. We’ll start by quickly implementing .catch()
in the Promise library, it’s just a convenience function for .then(undefined, failure())
.
Promise.prototype.catch = function(failure) {
return this.then(void 0, failure);
};
The Promise()
constructor doesn’t need to change at all, we’ll even keep the if (this._success) { ... }
and if (this._failure) { ... }
checks around the handler calls because at the end of the chain the handler functions won’t be defined. Our .then()
method needs to change quite substantially, the first and most important change is that to facilitate chaining .then()
and .catch()
handlers, we must always return a new Promise()
. Next we’ll always need to define this._success()
and this._failure()
methods. If .then()
provides a success handler we’ll call success()
with the passed in value, then resolve the new Promise with the result of success(value)
, otherwise we’ll just resolve the new Promise with the passed in value which just hands the passed in value along to the next handler in the chain. If .then()
provides a failure handler the processing is quite similar, we’ll call failure()
with the passed in error and then because the error should be handled after the failure handler, we resolve the new Promise with the result of failure(error)
. If there’s not a failure handler provide, we just pass the error down the chain. The revised .then()
method looks like this.
Promise.prototype.then = function(success, failure) {
return new Promise(
function(resolve, reject) {
// Always define this._success() to forward success value if not handled locally
this._success = function(value) {
if (success) {
// Handle success value locally, then pass on new success value
resolve(success(value));
} else {
// No local success handler, pass on success value
resolve(value);
}
};
// Always define this._failure() to forward error if not handled locally
this._failure = function(error) {
if (failure) {
// Handle error locally, then pass on new success value
resolve(failure(error));
} else {
// No local failure handler, pass on error
reject(error);
}
};
}.bind(this)
);
};
If you git checkout step2
and run the demonstration now, the functionality is unchanged, but the code to achieve it is a bit nicer.
Promises in the handler chain
One of the best things you can do with Promises is return a Promise within a handler, this lets you flatten out a chain of asynchronous operations. Take a look at script.js on branch step3
and you’ll notice that we’ve moved the .catch()
handler to the top of the chain and we’re returning a new Promise via another call to fetch()
, this time fetching a local fallback image.
fetch(imgUrl, { responseType: 'blob' }).catch(function(error) {
console.error('Got no image!', error);
console.info('Getting default image');
return fetch(defaultImgUrl, { responseType: 'blob' });
});
//...
We only need to make one change to our this._success()
handler inside the .this()
method. It now needs to check if the passed in value is an instance of Promise
, call .then()
on it if it is a Promise and resolve/reject the outer Promise based on the result/error of the passed in Promise. Replace the line resolve(success(value));
with the following block to handle Promises as a value.
if (value instanceof Promise) {
// Resolve/reject this Promise after the passed in Promise is resolved/rejected
value.then(
function(result) {
// Handle resulting value locally, then pass on new success value
resolve(success(result));
},
function(error) {
// Pass on resulting error
reject(error);
}
);
} else {
// Handle success value locally, then pass on new success value
resolve(success(value));
}
If you git checkout step3
and run the demonstration now, there’s one major change to the functionality. If the initial request to fetch imgUrl
fails, the .catch()
handler returns a new fetch request for defaultImgUrl
, this is a local fallback image so loading it shouldn’t fail. Try replacing the first fetch(imgUrl, { responseType: 'blob' });
with fetch(errorUrl, { responseType: 'blob' });
to demonstrate loading the fallback image and then continuing down the chain of handlers.
Slightly more advanced
Two very useful utility functions that a Promise library should provide are Promise.race()
and Promise.all()
. Both of these functions take an array of Promises and return a single Promise. Promise.race()
resolves or rejects it’s single returned Promise with the result or error from the first of it’s passed in Promises to resolve or reject. The rest of the passed in Promises are ignored, regardless if they resolve or reject. Promise.all()
resolves it’s single returned Promise with an array of results resolved from all of the passed in Promises. The indexes of the results array match the index of the Promise that resolved the result from the passed in array of Promises. If any of the Promises passed into Promise.all()
are rejected, the single returned Promise is immediately rejected with the error and all other passed in Promises are ignored, regardless if they are resolved or rejected. Promise.all()
looks like this.
Promise.all([
fetch(imgUrl, { responseType: 'blob' }), // fetch 0
fetch(imgUrl, { responseType: 'blob' }), // fetch 1
]).then(function(results) {
// results[0] will contain the result of fetch 0
// results[1] will contain the result of fetch 1
});
Try and think about how you might implement Promise.race()
and Promise.all()
. Take a look at script.js at branches step4
and step5
for the demonstration examples of Promise.race()
and Promise.all()
respectively. (git checkout step4
and git checkout step5
) to run them. Below are my implementations.
Promise.race()
Promise.race = function(promises) {
var settled = false; // Keep track if any Promises have been settled
return new Promise(function(resolve, reject) {
promises.forEach(function(promise) {
promise.then(
function(result) {
if (!settled) {
// First Promise to resolve/reject, resolve with result and set settled flag
resolve(result);
settled = true;
}
},
function(error) {
if (!settled) {
// First Promise to resolve/reject, reject with error and set settled flag
reject(error);
settled = true;
}
}
);
});
});
};
Promise.all()
Promise.all = function(promises) {
var settled = 0, // Keep track of the number of Promises settled
results = []; // Keep track of Promise resolutions
return new Promise(function(resolve, reject) {
promises.forEach(function(promise, index) {
promise.then(
function(result) {
// Add resolution to the same index as the Promises array
results[index] = result;
// Increment number of settled Promises, resolve when all are resolved
if (++settled === promises.length) {
resolve(results);
}
},
function(error) {
// Reject immediately if any Promises reject
reject(error);
}
);
});
});
};
Update 29/05/2015 Kyle Simpson (@getify) took some time to review this article and gave some awesome feedback. I’ve been slowly rewriting my Promises implementation, but I haven’t had as much time to work on it as I’d like so I thought I should post an update in the meantime. Firstly, this implementation isn’t currently Promises/A+ compliant, that was never an original goal, but it is something I’m working towards now. Kyle has a lightweight Promises/A+ compliant implementation called Native Promises Only (NPO). Two very important things to note about my current implementation:
- My handlers aren’t async! This is a big no-no, but relatively easily fixed by wrapping their execution in
setTimeout(..., 0)
- Promises should recursively resolve to eventually return a non-Promise value