This is a short article that formalizes a pattern I've used in other articles: that is encapsulating an event emitter within a Promise. I dub this pattern the Async Event Emitter Pattern.

While I won't go into the full details on EventEmitter class, I will cover some highlights as a reference point. As we should know, event emitters formalize a pub/sub model where the emitter object is the publisher and one or more subscribers can listen to many different events that can be fired zero or more times.

// creates an event emitter
const pub = new events.EventEmitter();

// adds a subscriber to the emitter for event1
pub.on("event1", () => console.log("event 1 fired"));

// fires event1 without any data
pub.emit("event1");

The neat thing about event emitters is that they are asynchronous by nature. We naturally defer the execution of the subscriber until the publisher has fired the event. This event can fire zero, one, or many different times.

As a subscriber we can choose to subscribe to the next event using the once method, or we can subscribe to all emissions of an event using on.  The neat thing is we can use the once to power a Promise.

Without getting into the nitty-gritty on Promises, they allow us to essentially pause execution in the current scope until the Promise has either succeeded or failed.  Inside the Promise, some code executes and then the Promise will either signal success or failure.

A simple example can wrap a timeout to delay execution in the calling function by awaiting the timeout completion:

function wait(ms = 1000): Promise<void> {
    return new Promise(resolve => {
        setTimeout(resolve, ms);
    });
}

async function run() {
    console.log("starting");
    await wait(1000);
    console.log("ending");
}

As you can see in the Promise accepts a function that has two methods that can be called:

  • resolve - resolves the promise with the supplied value
  • reject - rejects the promise with the supplied error

We can use new Promise to wrap any asynchronous code, including event emitters!

So how does this look? We'll start with a simple example:

function waitForEvent<T>(emitter, event): Promise<T> {
    return new Promise((resolve, reject) => {
        emitter.once(event, resolve);
        emitter.once("error", reject);        
    });
}

As you can see, we simply wrap an event emitter with a promise that resolve the first time our target event fires and rejects the first time there is an error.

The consumer can then use async/await to wait for the event!

async function run() {
    // create an event emitter
    const emitter = new EventEmitter();
    
    // fire the event in 1 second
    setTimeout(() => emitter.emit("greet", "hello world"), 1000);
    
    // wait for the event to fire!
    console.log("waiting for event...");
    let result = await waitForEvent(emitter, "greet");
    
    // event has now fired
    console.log("received", result);
}

As you can see the consumer of the emitter simply wraps the event and uses the much easier to understand async/await syntax.

There is a problem with this method though!  Because we are subscribing to two events but resolving the promise after one of the events (either the event has fired or the error has fired) we can be left with a dangling event subscription.

To clean that up, we need to unsubscribe the other event.  Unfortunately this makes the code a bit more cumbersome.

function waitForEvent<T>(emitter: EventEmitter, event: string): Promise<T> {
    return new Promise((resolve, reject) => {
        const success = (val: T) => {
            emitter.off("error", fail);
            resolve(val);
        };
        const fail = (err: Error) => {
            emitter.off(event, success);
            reject(err);
        };
        emitter.once(event, success);
        emitter.once("error", fail);
    });
}

Here we create two arrow function expressions that are used handle the success and failure scenarios. In each condition, we remove the event emitter for the opposing event prior to resolving or rejecting the promise.

This allows our calling function to handle either the success scenario:

async function run() {
    const emitter = new EventEmitter();
    setTimeout(() => emitter.emit("greet", "hello"), 1000);

    console.log("waiting for event");
    const result = await waitForEvent(emitter, "greet");
    console.log("emitted", result);    
}

Or a failure scenario:

async function run() {
    const emitter = new EventEmitter();
    setTimeout(() => emitter.emit("error", new Error("boom")), 1500);

    console.log("waiting for event");
    try {
        await waitForEvent(emitter, "greet");
    } catch (ex) {
        console.log("async failed with", ex);
    }
}

But both events won't fire

function run() {
    const emitter = new EventEmitter();

    setTimeout(() => {
        console.log("emitting error");
        emitter.emit("error", new Error("boom"));
    }, 500);

    setTimeout(() => {
        console.log("emitting greeting");
        emitter.emit("greet", "hello");
    }, 1000);

    waitForEvent(emitter, "greet")
        .then(val => console.log("greeted with", val))
        .catch(err => console.log("greeting failed with", err));
}

In this last scenario, because the error event is emitted first, the greet event will be emitted by the event emitter, but will not be captured by the Promise.

Conclusion

Hopefully you've found this pattern interesting. I've found it extremely useful for converting asynchronous event-based code into more manageable async/await code. It is also extremely helpful for building and working with state machines, especially ones built on socket streams or other async event emitters!