1/24
To summarize, this is what you will learn in this chapter: • The Callback pattern, how it works, what conventions are used in Node.js, and how to deal with its most common pitfalls • The Observer pattern and how to implement it in Node.js using the EventEmitter class
Name | Mastery | Learn | Test | Matching | Spaced |
---|
No study sessions yet.
The continuation-passing style (CPS)
In JavaScript, a callback is a function that is passed as an argument to another function and is invoked with the result when the operation completes. In functional programming, this way of propagating the result is called continuation-passing style (CPS).
It is a general concept, and it is not always associated with asynchronous operations. In fact, it simply indicates that a result is propagated by passing it to another function (the callback), instead of directly returning it to the caller.
Asynchronous CPS
Non-CPS callbacks
There are several circumstances in which the presence of a callback argument might make us think that a function is asynchronous or is using a CPS. That's not always true.
Unpredictable function (Synchronous & Asynchronous)
One of the most dangerous situations is to have an API that behaves synchronously under certain conditions and asynchronously under others.
Unleashing Zalgo
"Unleashing Zalgo" refers to a problem caused by functions that have unpredictable timing. Specifically, a function might sometimes execute synchronously (immediately) and other times asynchronously (after a delay), often depending on hidden conditions like whether data is cached.
Using synchronous APIs
The lesson to learn from the unleashing Zalgo example is that it is imperative for an API to clearly define its nature: either synchronous or asynchronous.
Using synchronous I/O in Node.js is strongly discouraged in many circumstances, but in some situations, this might be the easiest and most efficient solution. Always evaluate your specific use case in order to choose the right alternative. As an example, it makes perfect sense to use a synchronous blocking API to load a configuration file while bootstrapping an application.
That’s why deferring a synchronous function to an asynchronous one is alwasy the best option, We can do it in Nodejs by leveraging process.nextTick().
Guaranteeing asynchronicity with deferred execution
Another alternative for fixing our inconsistentRead() function is to make it purely asynchronous.
The trick here is to schedule the synchronous callback invocation to be executed "in the future" instead of it being run immediately in the same event loop cycle. In Node.js, this is possible with process.nextTick(), which defers the execution of a function after the currently running operation completes.
Its functionality is very simple: it takes a callback as an argument and pushes it to the top of the event queue, in front of any pending I/O event, and returns immediately. The callback will then be invoked as soon as the currently running operation yields control back to the event loop.
NodeJS callback conventions
In Node.js, CPS APIs and callbacks follow a set of specific conventions.
These conventions apply to the Node.js core API, but they are also followed by the vast majority of the userland modules and applications. So, it's very important that you understand them and make sure that you comply whenever you need to design an asynchronous API that makes use of callbacks.
The callback comes last
In all core Node.js functions, the standard convention is that when a function accepts a callback as input, this has to be passed as the last argument.
Any error always comes first
In Node.js, any error produced by a CPS function is always passed as the first argument of the callback, and any actual result is passed starting from the second argument. If the operation succeeds without errors, the first argument will be null or undefined.
Propagating errors
Propagating errors in synchronous, direct style functions is done with the well-known throw statement, which causes the error to jump up in the call stack until it is caught. In asynchronous CPS, however, proper error propagation is done by simply passing the error to the next callback in the chain.
Uncaught exceptions in asynchronous functions
In Node.js, errors thrown within the callback of an asynchronous function can sometimes go uncaught, especially if proper error handling mechanisms like try...catch
aren't used.
The observer pattern
The Observer pattern defines an object (called subject) that can notify a set of observers (or listeners) when a change in its state occurs.
The main difference from the Callback pattern is that the subject can actually notify multiple observers, while a traditional CPS callback will usually propagate its result to only one listener, the callback.
The EventEmitter
In traditional object-oriented programming, the Observer pattern requires interfaces, concrete classes, and a hierarchy. In Node.js, all this becomes much simpler.
The Observer pattern is already built into the core and is available through the EventEmitter class.
The EventEmitter class allows us to register one or more functions as listeners, which will be invoked when a particular event type is fired.
Creating and using the EventEmitter
Propagating errors with EventEmitter
As with callbacks, the EventEmitter can't just throw an exception when an error condition occurs. Instead, the convention is to emit a special event, called error, and pass an Error object as an argument. That's exactly what we were doing in the findRegex() function that we defined earlier.
The EventEmitter treats the error event in a special way. It will automatically throw an exception and exit from the application if such an event is emitted and no associated listener is found. For this reason, it is recommended to always register a listener for the error event.
Making any object observable
In the Node.js world, the EventEmitter is rarely used on its own, as you saw in the previous example. Instead, it is more common to see it extended by other classes. In practice, this enables any class to inherit the capabilities of the EventEmitter, hence becoming an observable object.
EventEmitter and memory leaks
When subscribing to observables with a long life span, it is extremely important that we unsubscribe our listeners once they are no longer needed. This allows us to release the memory used by the objects in a listener's scope and prevent memory leaks.
Unreleased EventEmitter listeners are the main source of memory leaks in Node.js (and JavaScript in general).
Memory leaks
A memory leak is a software defect whereby memory that is no longer needed is not released, causing the memory usage of an application to grow indefinitely. For example, consider the following code (img).
This means that if an EventEmitter remains reachable for the entire duration of the application, all its listeners do too, and with them all the memory they reference. If, for example, we register a listener to a "permanent" EventEmitter at every incoming HTTP request and never release it, then we are causing a memory leak. The memory used by the application will grow indefinitely, sometimes slowly, sometimes faster, but eventually it will crash the application. To prevent such a situation, we can release the listener with the removeListener() method of the EventEmitter:
Setting a maximum for registered events.
An EventEmitter has a very simple built-in mechanism for warning the developer about possible memory leaks. When the count of listeners registered to an event exceeds a specific amount (by default, 10), the EventEmitter will produce a warning. Sometimes, registering more than 10 listeners is completely fine, so we can adjust this limit by using the setMaxListeners() method of the EventEmitter.
We can use the convenience method once(event, listener) in place of on(event, listener) to automatically unregister a listener after the event is received for the first time. However, be advised that if the event we specify is never emitted, then the listener is never released, causing a memory leak.
Dealing with synchronous and asynchronous events
As with callbacks, events can also be emitted synchronously or asynchronously with respect to the moment the tasks that produce them are triggered. It is crucial that we never mix the two approaches in the same EventEmitter.
But even more importantly, we should never emit the same event type using a mix of synchronous and asynchronous code.
to avoid producing the same problems described in the Unleashing Zalgo section. The main difference between emitting synchronous and asynchronous events lies in the way listeners can be registered.
Emitting events asynchronously during the event loop cycle.
When events are emitted asynchronously, we can register new listeners, even after the task that produces the events is triggered, up until the current stack yields to the event loop. This is because the events are guaranteed not to be fired until the next cycle of the event loop, so we can be sure that we won't miss any events.
The emission of synchronous events can be deferred with process.nextTick() to guarantee that they are emitted asynchronously.
EventEmitter versus callbacks
A common dilemma when defining an asynchronous API is deciding whether to use an EventEmitter or simply accept a callback. The general differentiating rule is semantic:
callbacks should be used when a result must be returned in an asynchronous way,
while events should be used when there is a need to communicate that something has happened.
But besides this simple principle, a lot of confusion is generated from the fact that the two paradigms are, most of the time, equivalent and allow us to achieve the same results.
Hints on which is better to use between EventEmitter or callback style.
While a deterministic set of rules for you to choose between one style or the other can't be given, here are some hints to help you make a decision on which method to use:\
Callbacks have some limitations when it comes to supporting different types of events. In fact, we can still differentiate between multiple events by passing the type as an argument of the callback, or by accepting several callbacks, one for each supported event. However, this can't exactly be considered an elegant API. In this situation, the EventEmitter can give a better interface and leaner code.
The EventEmitter should be used when the same event can occur multiple times, or may not occur at all. A callback, in fact, is expected to be invoked exactly once, whether the operation is successful or not. Having a possibly repeating circumstance should make us think again about the semantic nature of the occurrence, which is more similar to an event that has to be communicated, rather than a result to be returned.
An API that uses callbacks can notify only one particular callback, while using an EventEmitter allows us to register multiple listeners for the same event.
Combining callbacks and events
There are some particular circumstances where the EventEmitter can be used in conjunction with a callback. This pattern is extremely powerful as it allows us to pass a result asynchronously using a traditional callback, and at the same time return an EventEmitter, which can be used to provide a more detailed account on the status of an asynchronous process.
The EventEmitter can also be combined with other asynchronous mechanisms such as promises (which we will look at in Chapter 5, Asynchronous Control Flow Patterns with Promises and Async/ Await). In this case, just return an object (or array) containing both the promise and the EventEmitter. This object can then be destructured by the caller, like this: {promise, events} = foo().