Skip to content

Instantly share code, notes, and snippets.

@dmvaldman
Last active June 1, 2024 00:20
Show Gist options
  • Save dmvaldman/12a7e46be6c3097aae31 to your computer and use it in GitHub Desktop.
Save dmvaldman/12a7e46be6c3097aae31 to your computer and use it in GitHub Desktop.
Promises as EventEmitters

Promises as EventEmitters

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 a success or an error event

And goes on (at 26:04),

A promise.addCallback(cb) is just API sugar for promise.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 and reject

  • The then method adds resolve and reject callbacks to the Promise.

    • The resolve callback will be called on a resolve event
    • The reject callback will be called on the reject event
  • The catch method is a "catch all" to intercept any reject 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.

@jeveloper
Copy link

That is a very concise design , thank you for sharing.

@paulisaac
Copy link

The most accessible and useful synopsis of Promises I've read yet. Thanks.

@sinakarimi
Copy link

Do you think the current implementations don't wrap around an EventEmitter because they need to be compatible on browsers as well?

@medikoo
Copy link

medikoo commented Feb 29, 2016

Truth is that what was initially implemented in (very early version) of Node.js as promise, was actually an event emitter.

But it's not true that promise (as it's understood today, and as it was drove by A+) is an event emitter. First big difference is that when you register callback for resolve event after it occurred, it will still be called (it's not the case for event emitter). Secondly there can be only one, resolve and reject resolutions (it's not repeated events).

@dmvaldman
Copy link
Author

@medikoo, thanks for the best explanation of differences I've heard yet

@RangerMauve
Copy link

How would this handle promises that are already resolved? This wouldn't work with promises that have listeners attached asynchronously. Cool way of explaining it, though.

@dmvaldman
Copy link
Author

@RangerMauve Honestly I don't know why this is a feature of Promises (or similarly a difference between "hot" and "cold" observables). Do you find yourself needing this feature? To me it seems more like gracefully supporting an anti-pattern to make Promises more user friendly.

@MHerszak
Copy link

I have several emitters and I was wondering how you would "attach" those emitters to several promise? I can see a good use case for it.

@xpepermint
Copy link

Excelent!

@VinayaSathyanarayana
Copy link

Thanks... Informative....I am looking to rate limit some api calls.
Need one the following:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment