Callbacks in Node.js

If you're getting started in Node.js, the first thing you probably noticed (if you're coming from an OO language like Java, or even from PHP or Python) was that Node handles things... differently.  When I first dove into Node code, the thing that confused me the most was callback functions - the standard way that Node.js handles asynchronous code.

Asynchronous By Default

One of the most important things to realize about Node.js is that it is asynchronous by default - no manual threading is required.  Many things that are handled synchronously in other languages, such as file reads, database queries, etc, are handled asynchronously in Node.  That means you can't just make the call and expect it to work in the next line of your code:

var fileContents = fs.readFile("test.txt"); console.log(fileContents); //undefined

In this code, fileContents is undefined because Node executes the next line of code before the call to readFile completes - that's what asynchronous means!  Instead of executing each line of code, waiting for it to complete, and moving on, anything in Node that is dependent on an external process is started, then the code immediately moves on.

Synchronous Functions

Now, you might be tempted to just use the Node.js synchronous functions; especially in the core libraries, there are often synchronous versions of the functions that behave in the "expected" fashion: "fs.readFileSync", for example.

This, however, is not considered best practice for Node, and these sync functions should generally only be used if you can't find a reasonable alternative.  This is because of the way Node functions at its core - what is termed the Event Loop.

The Event Loop

You may or may not have already heard the term "Event Loop" in your Node research.  Node.js is an "event-driven" language, which means that instead of executing procedurally, in order, Node's primary form of execution is in response to events - something happens, Node realizes it, and reacts.  The Event Loop is how Node manages these events.

When a call is made to fs.readFile (as above), instead of holding and waiting for that function to complete, Node will ask for access to the file, and then will simply stop execution and enter the Event Loop.  Once the process is in this state, it's not actually doing anything - it's simply waiting for something to happen that it cares about.  When the file read is complete and the data is ready for the process to consume, it is notified of this, and then re-enters the code to continue execution.

If some other event occurs in the meantime, Node will recognize that event and respond to it, regardless of the fact that is is also waiting on the file read.  This type of event-based asynchronous functionality allows Node to "do" many things at once, despite the fact that it is only a single process/thread.

For example, a Node-based web server can actually receive and respond to multiple requests at once.  If one request comes in, and Node needs information from the database, it can make that request, and while waiting for the information can respond to other incoming requests.

Callbacks

The standard way in Node to handle these events is in the form of "callback functions."  Since the code is entering a holding pattern while waiting for some function to complete, it can't simply resume execution once it's done - as shown above, code following an asynchronous call will execute immediately after making the call.  Instead of waiting to run the next line of code, Node async functions expect to receive a function that will be run after the call completes.

This is the core of Node.js: instead of code order determining chronological order, Node flows through callback functions until it reaches a point where there aren't any more, and then stops.  Using this information, we can fix our readFile call so that we actually have access to the information:

fs.readFile("test.txt", function (err, fileContents) { console.log(fileContents); //data from file! });

In this code, the function that is passed as a parameter to the call to fs.readFile is the callback function - if you look at the documentation for readFile, it expects its last parameter to be a function.  Once the process receives a notificaiton that this read is complete, it will exit the event loop and execute this function.

Error-First Callback Pattern

One last thing to note about Node.js callbacks: best-practice form is to have an "error-first" callback: in the callback function, the first parameter should be an error object, and if there was no error performing the call, then it will be empty.  This enables easy checking of errors in callback functions - in fact, you should ALWAYS check for errors in a callback function!

fs.readFile("test.txt", function (err, fileContents) { if (err) { throw err; } console.log(fileContents); //data from file! });

Note that in the callback that is given to fs.readFile, the first parameter of that function is "err".  Obviously, in production code you would want to have a real error handler for this problem, but this call to "throw" illustrates an important point - because this is an asynchronous call, if you don't check for this error, it will probably just disappear!

Share This