This is the fourth article from a new series about Node.js. In the last few chapters we covered some ways of controlling the flow of asynchrnous operations: the callback pattern, using the async
package to orchestrate them and using an in-memory work queue. In this article we'll cover a new way of managing the flow of your code, this time using events. Enjoy!
- Pedro Teixeira, CTO, YLD!
(Some of the code samples in this article you can find in this github repo).
Using an Event Emitter
The callback pattern works well for simple operations that have a start and an end state; but if you’re interested in state changes, a callback is not enough. Fortunately Node.js ships with an event emitter implementation. The event emitter pattern allows you to decouple the producers and consumers of events using a standard interface.
Consuming events
Let’s say that your Node.js process is somehow able to connect to a home automation system that can notify you of several events happening in your home. If the system can notify you of the main doorbell ringing, a door object could emit an event named “bell”. Any module could subscribe to doorbell rings by listening to that event on a given object:
In this case the home.door object is an event emitter. As a client of this object, you start listening to "bell" events by using the .on method and passing in a function that gets called whenever an event with that specific name happens.
Events can also bear some data.
For instance, let’s say that you want to get notified when a light bulb is turned on or off:
Here you can see that the “on” and the “off” events bear the room name. This is arbitrary data that is emitted by the emitter. Besides subscribing to events, you can also unsubscribe by using listener.removeListener, specifying the event type and the event listener function. To use it you will have to name the listener function like this:
Producing events
You can use the event emitter as a producer by constructing a JavaScript pseudo-class and use Node’s built-in util.inherits to setup the proper inheritance attributes. Here is an example of a clock event emitter that emits tic and toc events:
clock_emitter_class.js:
Here you can see that the clock itself is an event emitter, and it’s internally calling self.emit to emit arbitrary events (tic and toc in this case). Alongside each event, we're passing the current clock time (in milliseconds since the time it was started) by using the arguments in self.emit following the event name.
Clients of this Clock class can then do:
How an event emitter handles errors
As we’ve seen, an event type is defined by an arbitrary string. In that aspect, all events are treated equally, with one exception: errors.
If an event emitter emits one event for which it has no listeners, that event gets ignored. But, in the special case of the event name being “error”, the error is instead thrown into the event loop, generating an uncaught exception.If that happens, you can catch an uncaught exception by listening to the uncaught.Exception that the global process object emits:
When catching an uncaught exception, you should treat it as a permanent failure in your Node process: consider taking this opportunity to close the services in an orderly fashion, and eventually terminate the process.
Yes, shutdown your process when an uncaught exception occurs. Otherwise you will most probably end up with a resource leak due to the unknown state some Node objects will be in.
Listening to events once
What happens when, in the above example, more than one uncaught exception gets caught? Both will trigger the closeEverything function, which may bring problems on your shutdown procedure. In this case you should probably only consider the first instance of an uncaught exception happening. You can do that by using emitter.once
:
But now what happens when two uncaughtExceptions occur? The second one will find no event listener, and will trigger an immediate process shutdown, interrupting our beloved close. Everything that was taking care of an orderly shutdown. To avoid that we can still log every uncaught exception we get meantime by adding a second listener:
Synchronous event delivery
Node.js is asynchronous, but since no I/O is envolved in emitting an event, event delivery is treated synchronously.
emit_asynchronous.js:
If run this file you will get the following output:
So, when emitting events, bear in mind that the listeners will be called before emitter.emit returns.
Remembering this last bit can save you a lot of debugging time when using more than one listener for the same event.
Event delivery exceptions
Since the event delivery is synchronous to the emission, what happens when one listener accidently throws an exception? Let’s see:
emit_throws.js:
Running the code above will produce the following output:
This is bad news. It means that, when emitting events, if one of the listeners throws, depending on the order of the listener registration, the listener may not be notified. This means that the event emitter is a good pattern for logically decoupling producers and consumers at the API level, but in practice the event producers and consumers are somewhat coupled.
For a more decoupled approach you can use an in-memory queue, or even a persistent queue (to be discussed in a future article).
Next article
If you need to process a stream of data from a source, you need to create one such stream, or even repeatedly transform some data, Node has that abstraction for you: Streams. In the next article we’ll cover the several types of streams, their interface, how you can create them and then assemble these reusable streams to form data processing pipelines.
You can find all the previous posts on Flow Control here:
This article was extracted from the Flow Control Patterns, a book from the Node Patterns series.