Using EventEmitters to resolve Promises from afar in Node.js

A common-but-powerful pattern I've come to frequent is the utilisation of EventEmitter alongside a Promise, enabling me to resolve said promise from an entirely different part of my code base.

To demonstrate, let's revisit the basics of both an EventEmitter and a Promise.

EventEmitter

An EventEmitter is an object which can be used to "emit" named events which, in turn, cause Function objects ("listeners") to be called. Here's a speedy example that will log "foo", "bar", and "baz":

const { EventEmitter } = require('events') // this is a core Node package

const emitter = new EventEmitter() // create our emitter
emitter.on('data', console.log) // console.log any data coming in from the 'data' event

// send data
emitter.emit('data', 'foo')
emitter.emit('data', 'bar')
emitter.emit('data', 'baz')

Now that we have that in place, as long as we pass that same emitter around, different parts of our application can communicate asynchronously and without relying on a direct connection.

Promise

For a Promise, let's look at the most recent way of defining them. Those used to newer versions of Node.js (and JavaScript in general) will know of async functions:

async function foo () {
	await doSomeWork()
	await doSomeMore()
	const data = await getUnorderedData()
	data.sort()

	return data
}

const data = await foo()

A function with the async prefix will always return a Promise and has the added benefit of allowing us to use the await keyword to "block" while some asynchronous work is completed.

For our purposes, though, we're going to use a different way of defining a Promise: with callbacks. Using new Promise, we can get two functions (resolve and reject) to use to, well, either resolve or reject the Promise.

function foo () {
	return new Promise((resolve, reject) => {
		...
	})
}

You can be cheeky here, too, and make the callback an async function to get the goodness of await while still having the tighter control of the two functions.

Combining the two

So how does this tie in with our EventEmitter? Well now that we have a function that resolves our Promise, it's really easy to use an emitter inside. We can use the once method to register a one-time listener that removes itself once it's invoked.

function waitForNextData () {
	return new Promise((resolve, reject) => {
		emitter.once('data', resolve)
	})
}

🤯 Now we could call waitForNextData which would return a Promise which would resolve once some new data came in via our emitter!

const data = await waitForNextData()

This is an incredibly simple combination of JavaScript's asynchronous toolkit, but provides some sneaky tactics to use across larger projects when direct communication between components is either difficult or ill-advised. It's used heavily in @jpwilliams/remit, a microservices toolkit, to manage incoming and outgoing messages which may be received in a place far different from where they were sent. Also @jpwilliams/waitgroup, a tiny version of Golang's WaitGroup with promises, which is a great mini example of this technique.

December 11, 2019