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.