I was trying to understand JavaScript Promises by using various libraries (bluebird, when, Q) and other async approaches.
I read the spec, some blog posts, and looked through some code. I learned how to
use Promises, but their internals were still a mystery involving state-machiney
concepts of pending
, fulfilled
and rejected
states, buffering thenables
, and
what to do if such-and-such happens but only when X but not Y unless it's Tuesday, etc.
Then by chance I had a moment of clarity when I was listening to Ryan Dahl's original Node.js presentation at JSConf 2009. There was this several-second part (click to play externally on YouTube):
![ScreenShot](http://i.imgur.com/u91DJ6P.png =400x)
He (casually) explains,
A Promise is an
EventEmitter
which emits asuccess
or anerror
event
And goes on (at 26:04),
A
promise.addCallback(cb)
is just API sugar forpromise.addListener('success', cb);
The mystery of Promises quickly unraveled for me.
Instead of thinking of a Promise as a state machine, I saw it as simple design pattern
over an EventEmitter
.
In modern parlance: promise.addCallback
is promise.then
; success
and error
events are emitted when a Promise gets resolved or rejected.
Yet not one of the over 50 PromisesA+ implementations
creates a Promise
as an extension of an EventEmitter
. Conversely, here's the original formulation of Promises in Node v0.1.29 (Feb 2010).
Here's how I think about Promises now:
-
A Promise emits two events,
resolve
andreject
-
The
then
method addsresolve
andreject
callbacks to the Promise.- The
resolve
callback will be called on aresolve
event - The
reject
callback will be called on thereject
event
- The
-
The
catch
method is a "catch all" to intercept anyreject
event upstream
That's it. Below is a super simple Promise class written in less than 50 lines of code. It's meant to be instructive, not A+ compliant, though it could be with not much work.
var EventEmitter = require('events').EventEmitter;
class Promise extends EventEmitter {
// Define a Promise with a function taking two parameters:
// a `resolve` function and `reject` function
constructor(executor){
super(); // Extend the EventEmitter super class
// When `resolve` is called with a value, it emits a `resolve` event
// passing the value downstream. Similarly for `reject`
var resolve = (value) => { this.emit('resolve', value) };
var reject = (reason) => { this.emit('reject', reason) };
if (executor) executor(resolve, reject);
}
// Add downstream resolve and reject listeners
then (resolveHandler, rejectHandler) {
var promise = new Promise();
// When a `resolve` event upstream is fired, execute the `resolveHandler`
// and pass the `resolve` event downstream with the result
if (resolveHandler) {
var resolve = (data) => {
var result = resolveHandler(data);
promise.emit('resolve', result);
};
this.on('resolve', resolve);
}
// When a `reject` event upstream is fired, execute the `rejectHandler`
// and pass the `reject` event downstream with the result
if (rejectHandler) {
var reject = (data) => {
var result = rejectHandler(data);
promise.emit('reject', result);
};
this.on('reject', reject);
}
else {
// Downstream listeners always listen to `reject` so that an
// eventual `catch` can intercept them
this.on('reject', (data) => { promise.emit('reject', data); });
}
return promise;
}
// Handle an error from a rejected Promise upstream
catch (handler) {
this.on('reject', handler);
}
}
It's interesting to go back in time and read the discussion for removing the native Node.js Promise library (as of v0.1.30) in favor of waiting for an implementation to come from the open source community (6 years later, and it's finally a standard!). The problem of callback-hell was anticipated long before it had that name. It was the stewardship of @ry to suggest waiting for a user-land solution, and to favor making Node.js a little less opinionated.