If the V8 Engine from Google is the core engine of your Node.js application, then callbacks act as its circulatory system. They facilitate a smooth, non-blocking flow of asynchronous control within modules and applications. However, for callbacks to function effectively at scale, a standardized and reliable protocol is essential. The “error-first” callback, also referred to as an “errorback”, “errback”, or “node-style callback”, was introduced to address this requirement and has since become the norm for Node.js callbacks. This article aims to elucidate this pattern, its recommended practices, and what makes it so potent.
Why Establish a Standard?
The extensive usage of callbacks in Node.js harks back to a programming style predating JavaScript itself. Known as Continuation-Passing Style (CPS), this approach involves passing a “continuation function” (i.e., callback) as an argument to be executed once the remaining code has run. This mechanism allows different functions to transfer control asynchronously across an application.
Given Node.js’s reliance on asynchronous code for performance, having a dependable callback pattern is paramount. Without it, developers would grapple with maintaining diverse signatures and styles across modules. The error-first pattern was integrated into Node core to tackle this issue head-on and has since become the prevailing standard. While each use case may necessitate different responses and requirements, the error-first pattern can accommodate them all.
Defining an Error-First Callback
There are essentially two rules for defining an error-first callback:
The first argument of the callback reserves space for an error object. If an error occurs, it will be conveyed through this initial “err” argument.
The second argument of the callback is designated for any successful response data. In the absence of an error, “err” will be set to null, and the successful data will be provided in the second argument.
That’s the crux of it. It’s straightforward, isn’t it? Naturally, there are significant best practices to consider as well. But before delving into those, let’s illustrate this with a practical example using the basic method fs.readFile():
fs.readFile('/foo.txt', function(err, data) {
// TODO: Error Handling Still Needed!
console.log(data);
});
The fs.readFile() function reads from a specified file path and invokes your callback upon completion. If everything proceeds smoothly, the file contents are passed in the “data” argument. However, if an issue arises (e.g., the file doesn’t exist, permission is denied), the “err” argument will be populated with an error object detailing the problem.
It’s incumbent upon you, as the creator of the callback, to handle this error appropriately. You might choose to terminate the entire application by throwing an error, or you could propagate the error to the next callback in an asynchronous flow. The decision hinges on both the context and the desired behavior.
fs.readFile('/foo.txt', function(err, data) {
// If an error occurs, handle it (throw, propagate, etc)
if(err) {
console.log('Unknown Error');
return;
}
// Otherwise, log the file contents
console.log(data);
});
Err-ception: Propagating Your Errors
By passing errors to a callback, a function no longer needs to make assumptions about how those errors should be handled. For example, readFile() itself lacks insight into how critical a file read error might be to your specific application. It could be an anticipated occurrence or a catastrophic event. Instead of making such judgments internally, readFile() delegates the handling back to you.
Consistency with this pattern enables errors to be propagated upward as many times as necessary. Each callback can opt to ignore, address, or pass on the error based on the available information and context at its level.
if(err) {
// Handle "Not Found" by responding with a custom error page
if(err.fileNotFound) {
return this.sendErrorMessage('File Does not Exist');
}
// Ignore "No Permission" errors, as this controller recognizes them as insignificant
// Propagate all other errors (Express will catch them)
if(!err.noPermission) {
return next(err);
}
}
Slow Your Roll, Control Your Flow
Equipped with a robust callback protocol, you’re no longer confined to executing one callback at a time. Callbacks can be invoked in parallel, in a queue, sequentially, or any combination you envision. Whether you intend to read ten different files or make a hundred API calls, there’s no need to tackle them one by one.
The async library proves invaluable for advanced callback utilization. And thanks to the error-first callback pattern, integrating it is remarkably straightforward.
// Example from the caolan/async README
async.parallel({
one: function(callback){
setTimeout(function(){
callback(null, 1);
}, 200);
},
two: function(callback){
setTimeout(function(){
callback(null, 2);
}, 100);
}
},
function(err, results) {
// results: {one: 1, two: 2}
});
Bringing it all Together
To witness these concepts in action, explore additional examples on GitHub. Of course, you’re free to disregard this callback paradigm altogether and delve into the realm of promises… but that’s a discussion for another time.