Chapter 3 - Callbacks and Events

0.0(0)
studied byStudied by 0 people
learnLearn
examPractice Test
spaced repetitionSpaced Repetition
heart puzzleMatch
flashcardsFlashcards
Card Sorting

1/24

flashcard set

Earn XP

Description and Tags

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

Study Analytics
Name
Mastery
Learn
Test
Matching
Spaced

No study sessions yet.

25 Terms

1
New cards

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.

<ul><li><p>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).</p></li><li><p>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.</p></li></ul><p></p>
2
New cards

Asynchronous CPS

knowt flashcard image
3
New cards

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.

<p>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.</p>
4
New cards

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.

<p>One of the most dangerous situations is to have an API that behaves synchronously under certain conditions and asynchronously under others.</p>
5
New cards
<p>Unleashing Zalgo</p>

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.

<p>"Unleashing Zalgo" refers to a problem caused by functions that have <strong>unpredictable timing</strong>. Specifically, a function might sometimes execute <strong>synchronously</strong> (immediately) and other times <strong>asynchronously</strong> (after a delay), often depending on hidden conditions like whether data is cached.</p>
6
New cards

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().

<ul><li><p>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.</p></li><li><p>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.</p></li><li><p>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(). </p></li></ul><p></p>
7
New cards

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.

<ul><li><p>Another alternative for fixing our <em>inconsistentRead() </em>function is to make it purely asynchronous. </p></li><li><p>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<em> process.nextTick()</em>, which defers the execution of a function after the currently running operation completes. </p></li><li><p>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.</p></li></ul><p></p>
8
New cards

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.

9
New cards

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.

<p>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.</p>
10
New cards

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.

<p>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.</p>
11
New cards

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.

<p>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.</p>
12
New cards

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.

<p>In Node.js, errors thrown within the callback of an asynchronous function can sometimes go uncaught, especially if proper error handling mechanisms like <code>try...catch</code> aren't used.</p>
13
New cards

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.

14
New cards

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.

<p>In traditional object-oriented programming, the Observer pattern requires interfaces, concrete classes, and a hierarchy. In Node.js, all this becomes much simpler. </p><p>The Observer pattern is already built into the core and is available through the EventEmitter class. </p><p>The EventEmitter class allows us to register one or more functions as listeners, which will be invoked when a particular event type is fired.</p>
15
New cards

Creating and using the EventEmitter

knowt flashcard image
16
New cards

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.

17
New cards
<p>Making any object observable</p>

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.

<p>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.</p>
18
New cards

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).

19
New cards

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:

<p>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).</p><p>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, <mark data-color="green" style="background-color: green; color: inherit">we can release the listener with the removeListener() method of the EventEmitter:</mark></p>
20
New cards

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.

21
New cards

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.

22
New cards

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.

<p>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.</p><p>The emission of synchronous events can be deferred with process.nextTick() to guarantee that they are emitted asynchronously.</p>
23
New cards

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.

<p>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: </p><ul><li><p>callbacks should be used when a result must be returned in an asynchronous way, </p></li><li><p>while events should be used when there is a need to communicate that something has happened.</p></li></ul><p>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.</p>
24
New cards

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.

25
New cards

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().

<ul><li><p>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.</p></li><li><p>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().</p></li></ul><p></p>