A World Of Promises, episode 1
This article is the first of a series of small articles on the Q Javascript library and its eco-system. This article is a brief introduction to Q Promises.
Q is a Promise library in Javascript created 4 years ago by Kris Kowal who is one of the main contributor to CommonJS where we can find the Promises/A specification.
Q is probably the most mature and powerful Promise library in Javascript which inspired a lot of libraries like jQuery. It exposes a complete API with, in my humble opinion, good ideas like the separation of concerns between a "Deferred" object (the resolver) and a "Thenable" Promise (the read-only promise).
This article is a brief introduction to Q Promises with some examples. For more information on the subject, I highly recommend reading the article "You're Missing the Point of Promises" and the Q implementation design README.
What is a Promise
A Promise is an object representing a possible future value which has
a then
method to access this value via callback. A Promise is initially
in a pending state and is then either fulfilled with a value or rejected with an error.
Some properties
It is immutable because the Promise value never changes and each then
creates a new Promise.
As a consequence, one same Promise can be shared between different code.
It is chainable through the then
method (and other Q shortcut methods),
which transforms a Promise into a new Promise without knowing what's inside.
It is composable because the then
method will unify any Promise returned as
a result of the callback with the current Promise (act like a map or flatmap).
Q also has a Q.all
helper for combining an Array of Promise into one big Promise.
A solution against the Pyramid of Doom effect
Javascript is by nature an asynchronous language based on an event loop which enqueue events. As a consequence, there is no way to block long actions (like Image Loading, ajax requests, other events), but everything is instead asynchronous: Most of Javascript APIs are using callbacks - functions called when the event has succeeded.
Problem with callbacks is when you start having a lot of asynchronous actions. It quickly becomes a Callback Hell.
Example
Here is a simple illustration, let's say we have 2 functions,
one for retrieving some photos meta-infos from Flickr with a search criteria: getFlickrJson(search, callback)
,
another for loading an image from one photo meta-info: loadImage(json, callback)
.
Of-course both functions are asynchonous so they need a callback to be called with a result.
With this callback approach, we can then write:
// search photos for "Paris", load and display the first one
getFlickrJson("Paris", function (imagesMeta) {
loadImage(imagesMeta[0], function (image) {
displayImage(image);
});
});
(Imagine what it can look like with more nested steps.)
we can easily turn a callback API into a Promise API
Promise style
getFlickrJson
and loadImage
can now be rewritten as Promise APIs:
Each function has clean signatures:
getFlickrJson
is a(search: String) => Promise[Array of ImageMeta]
.loadImage
is a(imageMeta: ImageMeta) => Promise[DOM Image]
.displayImage
is a(image: DOM Image) => undefined
.
...and are easily pluggable together:
getFlickrJson("Paris")
.then(function (imagesMeta) { return imagesMeta[0]; })
.then(loadImage)
.then(displayImage, displayError);
This is much more flatten, concise, maintainable and beautiful!
Note that if we want to be safer we can write:
Q.fcall(getFlickrJson, "Paris")
.then(function (imagesMeta) { return imagesMeta[0]; })
.then(loadImage)
.then(displayImage, displayError);
Q.fcall
will call the function with the given parameters and ensure wrapping the returned value into a Promise.
So my code should continue working even if we change signatures to:
getFlickrJson
is a(search: String) => Array of ImageMeta
.loadImage
is a(imageMeta: ImageMeta) => DOM Image
.
One other cool thing about this chain of Promises is we can easily add more steps between two then
step, for instance a DOM animation, a little delay, etc.
Error Handling
But a much important benefit is, unlike the callbacks approach, we can properly handle the error in one row because one of the following steps eventually fails:
getFlickrJson
fails to perform the ajax request to retrieve the Flickr JSON data.- The array returned by Flickr was empty so
loadImage
throws an exception. - The
loadImage
fails (e.g. the image is unavailable).
This is called propagation and is exactly how exceptions work.
Promise Error Handling really looks like Exception Handling.
If it would be possible to have two methods:
getFlickrJsonSync
is a(search: String) => Array of ImageMeta
.loadImageSync
is a(imageMeta: ImageMeta) => DOM Image
.
Then, the blocking code would look like this:
try {
var imagesMeta = getFlickrJsonSync("Paris")
var firstImageMeta = imagesMeta[0]
var image = loadImageSync(firstImageMeta)
displayImage(image);
} catch (e) {
displayError(e);
}
...which is very close to Promise style.
Q Promises also unify Exceptions and Rejected Promises: throwing an exception in any Q callback will result in a rejected Promise.
var safePromise = Q.fcall(function () {
// following eventually throws an exception
return JSON.parse(someUnsafeJsonString);
});
// safePromise is either fulfilled with a JSON Object
// or rejected with an error.
Error handling with the callbacks approach is hell:
getFlickrJson("Paris", function (imagesMeta) {
if (imagesMeta.length == 0) {
displayError();
}
else {
loadImage(imagesMeta[0], function (image) {
displayImage(image);
}, displayError);
}
}, displayError);
Next episode
Next episode, we will show you how to create your own Promises with Deferred objects. We will introduce Qimage, a simple Image loader wrapped with Q.
Special Kudos to @42loops & @bobylito for bringing Q in my developer life :-P