# The callback pattern
Continuous-passing style
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).
An unpredictable function:
import { readFile } from "fs";
const cache = new Map();
function inconsistentRead(filename, cb) {
if (cache.has(filename)) {
// invoked synchronously
cb(cache.get(filename));
} else {
// asynchronous function
readFile(filename, "utf8", (err, data) => {
cache.set(filename, data);
cb(data);
});
}
}
Using synchronous APIs
Always choose a direct style for purely synchronous functions.
โ Use blocking APIs sparingly and only when they don't affect the ability of the application to handle concurrent asynchronous operations.
import { readFileSync } from "fs";
const cache = new Map();
function consistentReadSync(filename) {
if (cache.has(filename)) {
return cache.get(filename);
} else {
const data = readFileSync(filename, "utf8");
cache.set(filename, data);
return data;
}
}
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. Example: It makes perfect sense to use a synchronous blocking API to load a configuration file while bootstrapping an application.
Asynchronous behaviour with deferred execution
import { readFile } from "fs";
const cache = new Map();
function consistentReadAsync(filename, callback) {
if (cache.has(filename)) {
// deferred callback invocation
process.nextTick(() => callback(cache.get(filename)));
} else {
// asynchronous function
readFile(filename, "utf8", (err, data) => {
cache.set(filename, data);
callback(data);
});
}
}
A callback is invoked asynchronously by deferring its execution using process.nextTick()
.
process.nextTick() vs setImmediate()
Callbacks deferred with process.nextTick()
are called microtasks and they are executed just after the current operation completes, even before any other I/O event is fired.
Callbacks with setImmediate()
are queued in an event loop phase that comes after all I/O events have been processed.
Since, process.nextTick()
runs before any already scheduled I/O, it will be executed faster, but under certain circumstances, it might also delay the running of any I/O callback indefinitely (also known as I/O starvation), such as in the presence of a recursive invocation. This can never happen with setImmediate()
.
setImmediate()
are executed faster than those scheduled withsetTimeout(callback, 0)
.
NodeJS callback convention
readFile(filename, [options], callback)
callback
is of the format (err, data) => { ... }
. Here, err
must always be of type Error
.
Fail-fast approach
process.on("uncaughtException", (err) => {
console.error(`This will catch at last the JSON parsing exception: ${err.message}`);
// Terminates the application with 1 (error) as exit code.
// Without the following line, the application would continue
process.exit(1);
});
An uncaught exception leaves the application in a state that is not guaranteed to be consistent, which can lead to unforeseeable problems. That's why it is always advised, especially in production, to never leave the application running after an uncaught exception is received. Instead, the process should exit immediately, optionally after having run some necessary cleanup tasks, and ideally, a supervising process should restart the application. This is known as the fail-fast approach.
# 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.
import { EventEmitter } from 'events'
const emitter = new EventEmitter()
on(event, listener): EventEmitter
once(event, listener): EventEmitter
emit(event, [arg1], [...]): EventEmitter
removeListener(event, listener): EventEmitter
Propagating errors
- The convention is to emit a special event, called
error
, and pass anError
object as an argument.
Extending EventEmitter
class
To make any object observable, have the class extend the EventEmitter
class.
- Examples: Node.js streams, HTTP Module
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 thesetMaxListeners()
method of the EventEmitter.
Example:
class FindRegex extends EventEmitter {
constructor(regex) {
super();
this.regex = regex;
this.files = [];
}
addFile(file) {
this.files.push(file);
return this;
}
find() {
for (const file of this.files) {
readFile(file, 'utf8', (err, content) => {
if (err) {
return this.emit('error', err);
}
this.emit('fileread', file);
const match = content.match(this.regex);
if (match) {
match.forEach(elem => this.emit('found', file, elem));
}
});
}
}
return this;
}
Using the above code:
const findRegexInstance = new FindRegex(/hello \w+/);
findRegexInstance
.addFile('fileA.txt')
.addFile('fileB.json')
.find()
.on('found', (file, match) => console.log(`Matched "${match}" in file ${file}`))
.on('error', err => console.error(`Error emitted ${err.message}`));
Dealing with event emitters 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 to avoid memory leaks.
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.
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.
# Difference between callbacks and event emitters
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.
Top comments (0)