Promises provide a compelling alternative to raw callbacks when dealing with asynchronous code. Unfortunately, promises can be confusing and perhaps you've written them off. However, significant work has been done to bring out the essential beauty of promises in a way that is interoperable and verifiable. The result is Promises/A+, a specification that is making its way into promise libraries and even the DOM.
So what are promises all about and what do they offer when writing Node applications?
Let's talk about the behavior of promises first to establish what they are and how they can be useful. In the second half of this article, we will discuss how to create promises and use them in your applications with Q.
So what is a promise? Let's look at a definition:
A promise is an abstraction for asynchronous programming. It’s an object that proxies for the return value or the exception thrown by a function that has to do some asynchronous processing. -- Kris Kowal on JSJ
Callbacks are the simplest mechanism we could have for asynchronous code in JavaScript. Unfortunately raw callbacks sacrifice the control flow, exception handling and function semantics we are familiar with in synchronous code. Promises provide a way to get those things back.
The core component of a promise object is its then
method. The then
method is how we get the return value
(known as the fulfillment value) or the exception thrown (known as the rejection reason) from an asynchronous operation. then
takes two optional callbacks as arguments, which we'll call onFulfilled
and onRejected
:
var promise = doSomethingAync()
promise.then(onFulfilled, onRejected)
onFulfilled
and onRejected
are called when the promise is resolved (the asynchronous processing has completed). Only one will ever be triggered since only one resolution is possible.
Given this basic knowledge of promises, let's take a look at a familiar asynchronous Node callback:
readFile(function (err, data) {
if (err) return console.error(err)
console.log(data)
})
If our readFile
function returned a promise we would write the same logic as:
var promise = readFile()
promise.then(console.log, console.error)
At first glance, it looks like just the aesthetics changed. However, we now have access to a value representing the asynchronous operation (the promise). We can pass the promise around and anyone with access to the promise can consume it using then
regardless if the asynchronous operation has completed or not. We also have guarantees that the result of the asynchronous operation won't change somehow, as the promise will only be resolved once (either fulfilled or rejected).
It's helpful to think of
then
not just as a function that takes two callbacks (onFulfilled
andonRejected
), but as a function that unwraps the promise to reveal what happened from the asynchronous operation. Anyone with access to the promise can usethen
to unwrap it. Read more about this idea here.
The then
method itself returns a promise:
var promise = readFile()
var promise2 = promise.then(readAnotherFile, console.error)
This promise represents the return value for its onFulfilled
or onRejected
handlers if specified. Since only one resolution is possible, the promise proxies whichever handler is called:
var promise = readFile()
var promise2 = promise.then(function (data) {
return readAnotherFile() // if readFile was successful, let's readAnotherFile
}, function (err) {
console.error(err) // if readFile was unsuccessful, let's log it but still readAnotherFile
return readAnotherFile()
})
promise2.then(console.log, console.error) // the result of readAnotherFile
Since then
returns a promise, it means promises can be chained to avoid callback hell:
readFile()
.then(readAnotherFile)
.then(doSomethingElse)
.then(...)
Promises can be nested as well if keeping a closure alive is important:
readFile()
.then(function (data) {
return readAnotherFile().then(function () {
// do something with `data`
})
})
Promises model synchronous functions in a number of ways. One such way is using return
for continuation instead of calling another function. In the previous examples, readAnotherFile()
was returned to signal what to do after readFile
was done.
If you return a promise, it will signal the next then
when the asynchronous operation completes. You can also return any other value and the next onFulfilled
will be passed the value as an argument:
readFile()
.then(function (buf) {
return JSON.parse(buf.toString())
})
.then(function (data) {
// do something with `data`
})
In addition to return
, you also can use the throw
keyword and try/catch
semantics. This may be one of the most powerful features of promises. To illustrate this, let's say we had the following synchronous code:
try {
doThis()
doThat()
} catch (err) {
console.error(err)
}
In this example, if doThis()
or doThat()
would throw
an error, we would catch
and log the error. Since try/catch
blocks allow multiple operations to be grouped, we can avoid having to explicitly handle errors for each operation. We can do this same thing asynchronously with promises:
doThisAsync()
.then(doThatAsync)
.then(null, console.error)
If doThisAsync()
is unsuccessful, its promise will be rejected and the next then
in the chain with an onRejected
handler will be called. In our case, this is the console.error
function. And just like try/catch
blocks, doThatAsync()
would never get called. This is a huge improvement over raw callbacks in which errors must be handled explicitly at each step.
However, it gets better! Any thrown exception, implicit or explicit, from the then
callbacks is also handled in promises:
doThisAsync()
.then(function (data) {
data.foo.baz = 'bar' // throws a ReferenceError as foo is not defined
})
.then(null, console.error)
Here the raised ReferenceError
will be caught by the next onRejected
handler in the chain. Pretty neat! Of course this works for explicit throw
as well:
doThisAsync()
.then(function (data) {
if (!data.baz) throw new Error('Expected baz to be there')
})
.then(null, console.error)
As stated earlier, promises mimic try/catch
semantics. In a try/catch
block, it's possible to mask an error by never explicitly handling it:
try {
throw new Error('never will know this happened')
} catch (e) {}
The same goes for promises:
readFile()
.then(function (data) {
throw new Error('never will know this happened')
})
To expose masked errors, a solution is to end the promise chain with a simple .then(null, onRejected)
clause:
readFile()
.then(function (data) {
throw new Error('now I know this happened')
})
.then(null, console.error)
Libraries include other options for exposing masked errors. For example, Q provides the done method to rethrow the error upstream.
So far, our examples have used promise-returning dummy methods to illustrate the then
method from Promises/A+. Let's turn now and look at more concrete examples.
You may be wondering how a promise is generated in the first place. The API for creating a promise isn't specified in Promise/A+ because its not necessary for interoperability. Therefore, promise libraries will likely vary in implementation. Our examples focus on the excellent Q library (npm install q
).
Node core asynchronous functions do not return promises; they take callbacks. However, we can easily make them return promises using Q:
var fs_readFile = Q.denodify(fs.readFile)
var promise = fs_readFile('myfile.txt')
promise.then(console.log, console.error)
Q provides a number of helper functions for adapting Node and other environments to be promise aware. Check out the readme and API documentation for more details.
You can manually create a promise using Q.defer
. Let's say we wanted to manually wrap fs.readFile
to be promise aware (this is basically what Q.denodify
does):
function fs_readFile (file, encoding) {
var deferred = Q.defer()
fs.readFile(file, encoding, function (err, data) {
if (err) deferred.reject(err) // rejects the promise with `er` as the reason
else deferred.resolve(data) // fulfills the promise with `data` as the value
})
return deferred.promise // the promise is returned
}
fs_readFile('myfile.txt').then(console.log, console.error)
We have seen two ways to turn callback code into promise code. You can also make APIs that provide both a promise and callback interface. For example, let's turn fs.readFile
into an API that supports both callbacks and promises:
function fs_readFile (file, encoding, callback) {
var deferred = Q.defer()
fs.readFile(function (err, data) {
if (err) deferred.reject(err) // rejects the promise with `er` as the reason
else deferred.resolve(data) // fulfills the promise with `data` as the value
})
return deferred.promise.nodeify(callback) // the promise is returned
}
If a callback is provided, it will be called with the standard Node style (err, result)
arguments when the promise is rejected or resolved.
fs_readFile('myfile.txt', 'utf8', function (er, data) {
// ...
})
So far, we've only talked about sequential asynchronous operations. For parallel operations, Q provides the Q.all
method which takes in an array of promises and returns a new promise. The new promise is fulfilled after all the operations have completed successfully. If any of the operations fail, the new promise is rejected.
var allPromise = Q.all([ fs_readFile('file1.txt'), fs_readFile('file2.txt') ])
allPromise.then(console.log, console.error)
It's important to note again that promises mimic functions. A function only has one return value. When passing
Q.all
two promises that complete successfully,onFulfilled
will be called with only one argument (an array with both results). This may surprise you; however, consistency with synchronous counterparts is an important guarantee that promises provide. If you want to spread results out into multiple arguments, Q.spread can be used.
The best way to really understand promises is to use them. Here are some ideas to get you started:
- Wrap some basic Node workflows converting callbacks into promises
- Rewrite one of the async methods into one that uses promises
- Write something recursively using promises (a directory tree might be a good start)
- Write a passing Promise A+ implementation. Here is my crude one.