Skip to content

Instantly share code, notes, and snippets.

@Rho-bur
Forked from dmvaldman/promisesEM.md
Created November 3, 2021 08:54
Show Gist options
  • Save Rho-bur/3f4684626ee7c4e4aa08e181e208d884 to your computer and use it in GitHub Desktop.
Save Rho-bur/3f4684626ee7c4e4aa08e181e208d884 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.

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