Use the Node.js event loop effectively
How to avoid unexpected results in your Node.js apps
Interactive code: When you see Run at the top of a code sample, you can run the code, check the results,
make changes, and run it again.
For new Node.js application developers, part of the learning curve is getting comfortable with how the single-threaded event loop works and how it can lead to unexpected results. You can practice using the event loop in the 3 interactive samples in this tutorial. In no time, you'll be writing fast, efficient code that handles asynchronous calls easily.
Let's look at three simple pieces of code to illustrate how the event loop works.
Sample 1: A simple example
The
first sample defines three functions and calls them. Click
Run to run the code. Then try changing the numeric
values in the
setTimeout()
calls to see how the output
changes. For example, set all the values to 0.
You might have expected the original sample to generate this output:
world! Hello there,
However, that's not the case. printNow('Hello')
executes
first, then printSoon('there,')
, then
printEventually('world!')
. That's because when the Node
engine sees the setTimeout()
in the
printEventually()
call, it hands it to the OS and then moves
on to the call to printNow()
. That code is synchronous, so
the message Hello
prints right away. Finally, the call to
setTimeout()
in printSoon()
is passed to the OS.
One second later (printSoon()
waits for 1000 milliseconds),
the setTimeout()
timer in printSoon()
expires,
so the message there,
prints to the console. One second after
that, the last setTimeout()
expires and world!
shows up. That's why the three statements are processed in this order:
Hello there, world!
How the event loop works
Traditional web servers are multithreaded, with each session typically getting its own thread. That approach works, but it requires the web server to allocate resources that sit unused when the session is idle. The overhead of those idle sessions makes it more difficult to scale the server to handle spikes in demand.
The Node engine, on the other hand, has a single thread that handles every notification from the operating system that something is ready to be acted upon. If that thing is asynchronous (a call to a database or a REST interface, for example), the Node engine asks the operating system to notify it when that call is ready for processing (say, when data has arrived from the database or the REST call). In the meantime, the Node event loop moves on to the next thing that needs to be done.
Understand that the Node engine handles everything right away. It's just that in some cases "right away" means asking the operating system to let it know when something is ready for processing. For more information about how the magic happens, see the videos listed in the "Related topics" section at the end of this tutorial.
Sample 2: The callback pattern
Although the first sample illustrates how Node handles asynchronous code, you normally call asynchronous code with the callback pattern. The pattern looks like this:
Listing 1. Pseudocode for defining an asynchronous function
function asyncCode(arg1, arg2, callback) { // Do whatever the function does here. When it's complete, it should // invoke the callback function, returning a (hopefully null) // JavaScript Error object and the results of this function. return callback(error, results); }
The last parameter passed to asyncCode()
is another
function. When asyncCode()
finishes its work, it
invokes the callback function passed to it. By convention, the
asynchronous function passes a JavaScript Error
object as the
first parameter to the callback, followed by whatever results the
asynchronous function generated. Note that the asyncCode()
function can have as many arguments as it needs, and it can pass as many
arguments to the callback function as necessary.
That's how to define an asynchronous function. Here's how to call one:
Listing 2. Pseudocode for calling an asynchronous function
asyncCode(x, 37, function(error, results) { if (error != null) // Handle the error if there is one else // Otherwise do whatever we want with the results });
This
version of the code uses callback functions. Click Run to
run the code as is. Then try changing the numeric values in the
printMessage()
calls to see how the output changes. Try
replacing console.log('Hello')
with another call to
printMessage()
. What happens if the value of a
timeout
is zero? Does that affect the order of execution?
The printMessage()
function implements the callback pattern.
It sets a timeout, causing Node to pass the timeout to the OS. Node then
moves on to the next thing. In this case the next thing is a simple call
to console.log()
. After that is another call to
printMessage()
, which sets another timeout. The code wraps up
as the timeouts expire and there,
and world!
are
written to the console. The callback functions generate the same message
as the first sample:
Hello there, world!
Sample 3: Nesting callbacks
If for some reason you wanted the three words of the message to print in a
particular order, you would need to nest the callback functions. For
example, if the timeout
parameters were randomly generated
numbers between 0 and 5000, you would have no way of knowing how your
message would turn out.
Click
Run to run the code as is. Now try changing the
numeric values in the
printMessage()
calls. No matter what
values you use, the code executes in the same order.
This code ensures that these three invocations of
printMessage()
occur in a particular order. The first call to
printMessage()
passes in a callback function that also calls
printMessage()
, which in turn passes in another callback
function that calls printMessage()
. The code generates this
garbled greeting:
world! Hello there,
The code is relatively easy to read because we ignore error handling and
have only one line of code before calling printMessage()
again. If we added error handling back to the code and have complex logic
between calls, this can quickly lead to callback hell, where code
is nested to several levels and difficult to understand. See the "Related
topics" section for more on callback hell.
Conclusion
That's a quick look at how to use the Node.js single-threaded event loop. As you use Node libraries to access things like databases and files, knowing how to handle asynchronous methods — and how to ensure that code is executed in a certain order — are crucial skills. With a good understanding of the event loop, you'll be able to write fast, efficient code that handles asynchronous calls easily.
About our interactive samples
The interactive code samples in this tutorial use the developerWorks sandbox. The sandbox is deployed in an OpenWhisk environment running on IBM Bluemix®. Look for lots more interactive content over the coming weeks and months. (And as always, we encourage you to sign up for a free Bluemix account and see what the IBM Cloud platform can do.)
Acknowledgments
Your author would like to thank IBM's Sam Roberts for his help in correcting the errors and misconceptions in the original version of this tutorial. Any remaining falsehoods are solely the fault of the author.
Downloadable resources
Related topics
- Video: Everything you need to know about the node.js event loop, by IBM's Bert Belder
- Video: Node's event loop from the inside out, by IBM's Sam Roberts
- Callback hell is discussed in more detail at the aptly named callbackhell.com
- The JavaScript Error object
- Web development hub on developerWorks
- Node.js Developer Center
- Getting started with Node.js
- A Node.js developer's guide to Bluemix fundamentals
- More tutorials for Node.js developers
- Node.js journeys on developer.ibm.com/code