...

/

Untitled Masterpiece

Learn about Node.js conventions in the callback pattern.

In Node.js, CPS APIs and callbacks follow a set of specific conventions. These conventions apply to the Node.js core API, but they’re also followed by the vast majority of the userland modules and applications. So, it’s very important that we understand them and make sure that we comply whenever we 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.

Let’s take the following Node.js core API as an example:

readFile(filename, [options], callback)

As we can see from the signature of the preceding function, the callback is always put in the last position, even in the presence of optional arguments. The reason for this convention is that the function call is more readable in case the callback is defined in place.

Any error always comes first

In CPS, errors are propagated like any other type of result, that is, using callbacks. 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. The following code shows how to define a callback that complies with this convention:

readFile('foo.txt', 'utf8', (err, data) => {
if(err) {
handleError(err)
} else {
processData(data)
}
})

It’s best practice to always check for the presence of an error because not doing so will make it harder for us to debug our code and discover the possible points of failure. Another important convention to take into account is that the error must always be of the type Error. This means that simple strings or numbers should never be passed as error objects.

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’s caught.

In asynchronous CPS, however, proper error propagation is done by simply passing the error to the next callback in the chain. The typical pattern looks as follows:

Node.js
Files
import { readFile } from 'fs'
function readJSON (filename, callback) {
readFile(filename, 'utf8', (err, data) => {
let parsed
if (err) {
// propagate the error and exit the current function
return callback(err)
}
try {
// parse the file contents
parsed = JSON.parse(data)
} catch (err) {
// catch parsing errors
return callback(err)
}
// no errors, propagate just the data
callback(null, parsed)
})
}
const cb = (err, data) => {
if (err) {
return console.error(err)
}
console.log(data)
}
readJSON('valid_json.json', cb) // dumps the content
readJSON('invalid_json.json', cb) // prints error (SyntaxError)

Notice how we propagate the error received by the readFile() operation. We don’t throw it or return it; instead, we just use the callback as if it were any other result. Also, notice how we use the try...catch statement to catch any error thrown by JSON.parse(), which is a synchronous function and therefore uses the traditional throw instruction to propagate errors to the caller. Lastly, if everything went well, callback is invoked with null as the first argument to indicate that there are no errors.

It’s also interesting to note how we refrained from invoking callback from within the try block. This is because doing so would catch any error thrown from the execution of the callback itself, which is usually not what we want.

Uncaught exceptions

Sometimes, it can happen that an error is thrown and not caught within the callback of an asynchronous function. This could happen if, for example, we had forgotten to surround JSON.parse() with a try...catch statement in the readJSON() function we defined previously. Throwing an error inside an asynchronous callback would cause the error to jump up to the event loop, so it would never be propagated to the next callback. In Node.js, this is an unrecoverable state and the application would simply exit with a non-zero exit code, printing the stack trace to the stderr interface.

To demonstrate this, let’s try to remove the try...catch block surrounding JSON.parse() from the readJSON() function we defined previously.

function readJSONThrows(filename, callback) {
readFile(filename, "utf8", (err, data) => {
if (err) {
return callback(err);
}
callback(null, JSON.parse(data));
});
}

Now, in the function we just defined, there’s no way of catching an eventual exception coming from JSON.parse(). If we try to parse an invalid JSON file with the following code:

readJSONThrows("invalid_json.json", (err) => console.error(err));

This will result in the application being abruptly terminated, with a stack trace similar to the following being printed on the console:

SyntaxError: Unexpected token h in JSON at position 1
at JSON.parse (<anonymous>)
at file:///.../03-callbacks-and-events/08-uncaught-errors/index.
js:8:25
at FSReqCallback.readFileAfterClose [as oncomplete] (internal/fs/
read_file_context.js:61:3)
Stack trace as a result of propagating errors without the try...catch block

Now, if we look at the preceding stack trace, we’ll see that it starts from within the built-in fs module, and exactly from the point in which the native API has completed reading and returned its result back to the fs.readFile() function, via the event loop. This clearly shows that the exception traveled from our callback, up the call stack, and then straight into the event loop, where it was finally caught and thrown to the console.

This also means that wrapping the invocation of readJSONThrows() with a try...catch block will not work, because the stack in which the block operates is different from the one in which our callback is invoked. The following code shows the anti-pattern that was just described:

try {
readJSONThrows("invalid_json.json", (err) => console.error(err));
} catch (err) {
console.log("This will NOT catch the JSON parsing exception");
}

The preceding catch statement will never receive the JSON parsing error because it’ll travel up the call stack in which the error was thrown, that is, in the event loop and not in the function that triggered the asynchronous operation.

As mentioned previously, the application will abort the moment an exception reaches the event loop. However, we still have the chance to perform some cleanup or logging before the application terminates. In fact, when this happens, Node.js will emit a special event called uncaughtException, just before exiting the process.

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);
});

The following code shows a sample use case:

Node.js
Files
import { readFile } from "fs";
function readJSONThrows(filename, callback) {
readFile(filename, "utf8", (err, data) => {
if (err) {
return callback(err);
}
callback(null, JSON.parse(data));
});
}
// The error is not propagated to the final callback nor is caught
// by a try/catch statement
try {
readJSONThrows("invalid_json.json", (err) => console.error(err));
} catch (err) {
console.log("This will NOT catch the JSON parsing exception");
}
// Our last chance to intercept any uncaught error
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);
});

It’s important to understand that an uncaught exception leaves the application in a state that is not guaranteed to be consistent, which can lead to unforeseeable problems. For example, there might still be incomplete I/O requests running or closures might have become inconsistent. That’s why it’s 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 also known as the fail-fast approach and it’s the recommended practice in Node.js.

This concludes our gentle introduction to the Callback pattern.

Access this course and 1200+ top-rated courses and projects.