Events and Timers in Node.js
Node.js has multiple utilities for handling events as well as scheduling the execution of code. These utilities, combined, give you the ability to reactively respond at the right time, for example:
- Clearing session data when a user logs out
- Scheduling a timeout for receiving results from an API call and specifying the error handling code to run in case of timeout
- Closing database connections before exiting Node.js
In this article, we will go over how timers work in Node.js. We will also introduce how the Node.js event loop works and how you can take advantage of Node's event handling capabilities.
Timers
The first set of utilities we will look at are the setTimeout
, setImmediate
, and setInterval
timing utilities. With these tools, we can control the timing of code execution.
Why would this be important? In Node.js, similar to when using other programming languages such as C, Python, Java, and others, it helps to schedule certain functions to run repeatedly.
Suppose, for example, we want to copy certain files from a receiving location to a permanent archive. This would be a good scenario for scheduling a file transfer. At certain intervals, we can check for new files, and then copy them over to the backup location if there are any.
setTimeout
With setTimeout
, we can schedule code to be executed after a certain length of time has passed.
// setTimeout.js
let cue = 'The actors are here!';
// However, the cue is not announced until at least 5000ms have
// passed through the use of setTimeout
setTimeout(function() {
return console.log(cue);
}, 5000);
// This console log is executed right away
console.log('An exploration of art and music. And now, as we wait for the actors...');
To execute this code and see it in action, run node setTimeout.js
at your terminal:
$ node setTimeout.js
An exploration of art and music. And now, as we wait for the actors...
The actors are here!
Notice how even though the console('An exploration...')
call is after our console.log(cue)
call, it is still executed first.
The trick to realize here is that the code is only guaranteed to execute after at least that length of time has passed, not right on the dot.
setInterval
In situations where you need repeated, regular, code execution, such as long polling, then setInterval
method will be a more natural fit than setTimeout
. With this function, we can specify a function to be executed every X seconds. The function actually takes its argument in milliseconds, so you have to do the conversion yourself before entering in your arguments.
Suppose we want to check the length of the queue at a McDonald's drive-through so users of our program can dash out at the best time. Using setInterval
, we can repeatedly check the length of the queue and tell them when the coast is clear.
// setInterval.js
// This function simulates us checking the length
// of a McDonald's drive-through queue
let getQueueLength = function() {
return Math.round(12 * Math.random());
};
// We would like to retrieve the queue length at regular intervals
// this way, we can decide when to make a quick dash over
// at the optimal time
setInterval(function() {
let queueLength = getQueueLength();
console.log(`The queue at the McDonald's drive-through is now ${queueLength} cars long.`);
if (queueLength === 0) {
console.log('Quick, grab your coat!');
}
if (queueLength > 8) {
return console.log('This is beginning to look impossible!');
}
}, 3000);
You can see the output below. Run the code with node setInterval.js
, as shown below:.
$ node setTimeout.js
The queue at the McDonald's drive-through is now 6 cars long.
The queue at the McDonald's drive-through is now 0 cars long.
Quick, grab your coat!
The queue at the McDonald's drive-through is now 1 cars long.
The queue at the McDonald's drive-through is now 3 cars long.
The queue at the McDonald's drive-through is now 9 cars long.
This is beginning to look impossible!
The queue at the McDonald's drive-through is now 0 cars long.
Quick, grab your coat!
The queue at the McDonald's drive-through is now 10 cars long.
This is beginning to look impossible!
setImmediate
If we'd like a function to be executed as urgently as possible, we use setImmediate
. The function we execute this way will execute ahead of all setTimeout
or setInterval
calls as soon as the current Node.js event loop has finished calling event callbacks.
Here's an example of this in process. You can run this code with the command node setImmediate.js
// setImmediate.js
// A timeout
setTimeout(function() {
console.log('I am a timeout');
}, 5000);
// An interval
setInterval(function() {
console.log('I am an interval');
}, 5000);
// An immediate, its callback will be executed before those defined above
setImmediate(function() {
console.log('I am an immediate');
});
// IO callbacks and code in the normal event loop runs before the timers
console.log('I am a normal statement in the event loop, guess what comes next?');
$ node setImmediate.js
I am a normal statement in the event loop, guess what comes next?
I am an immediate
I am a timeout
I am an interval
I am an interval
I am an interval
...
The setImmediate
callback, though defined after those for setInterval
and setTimeout
, will run ahead of them.
The Event Loop
A question that might have occurred to you is "How does Node.js keep track of all these times, timers, and events? How is the order of execution prioritized?" This is a good line of inquiry and necessitates looking at something known as the "Node.js Event Loop".
So, what is the Event Loop?
The Event Loop is simply a repeated cycle by which Node.js switches through processing of computations. Since it cannot conduct all possible computations simultaneously, being single-threaded, it switches from computation to computation in a well-defined loop known as the Event Loop.
The Event Loop has the following basic stages:
- Timers - executes callbacks that have been scheduled with
setTimeout
andsetInterval
- Pending callbacks - executes any callbacks that are ready to run
- Idle, prepare - internal to Node.js
- Poll - accepts incoming connections and data processing
- Check - invokes callbacks set using
setImmediate
- Close callbacks - runs callbacks for close events
The Event Loop is the backbone of working with events and other asynchronous callbacks in Node.js. It allows us to lay hooks at certain points that will be hit in the course of the loop.
Responding to Asynchronous Returns with Callbacks
Given the single-threaded nature of Node.js, long-running operations such as file reads or database queries are quickly offloaded to the operating system, then Node.js continues its Event Loop as normal. This keeps things efficient and fast.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
How do those operating system processes interact with the Node.js process? Through the means of callbacks. We use a callback to asynchronously process things in the background, then hook back into the Event Loop once the asynchronous job has completed. To get this kind of functionality in other programming languages, you might use a task queue like Celery in Python or Sidekiq in Ruby. In Node.js, because the Event Loop and the asynchronous execution of Node.js already queue things up for you automatically, you get this asynchronous processing for free.
To see callbacks in action, we are going to read a file from the file system and use a call back to print out the contents.
The first step is to create the file. In this case, we are using a text file, containing the lines to a poem by T.S. Eliot. You can substitute your own file. This file is named poem.txt
and you can place the following contents into it.
// poem.txt
Macavity - The Mystery Cat, by T. S. Eliot
Macavity's a Mystery Cat: he's called the Hidden Paw--
For he's the master criminal who can defy the Law.
He's the bafflement of Scotland Yard, the Flying Squad's despair:
For when they reach the scene of crime--Macavity's not there!
Macavity, Macavity, there's no one like Macavity,
He's broken every human law, he breaks the law of gravity.
His powers of levitation would make a fakir stare,
And when you reach the scene of crime--Macavity's not there!
You may seek him in the basement, you may look up in the air--
But I tell you once and once again, Macavity's not there!
In the same directory, we will create our script that will read this poem file and print it back out. Printing the file or handling an error will be accomplished for us in a callback, after the operating system returns the result of the file read. As shown below, in readFile.js
, your callback gets triggered after the asynchronous operating system process returns. When this OS process returns, the Node.js callback you provided is placed on the event loop to be processed, which will then get executed when the loop gets to that process.
Your callback can then do anything from updating state in the application, to handling an error, if any, and logging the user out, doing nothing, or even terminating the Node process entirely.
// readFile.js
const fs = require('fs');
// Attempt to read the poem file
// Attach a callback to handle a successful read and print the contents to console
fs.readFile('./poem.txt', 'utf-8', function(err, data) {
if (err) return console.error(err);
let poem = data.toString();
console.log('Here is the poem of the day...\n\n');
return console.log(data);
});
Run this code with node readFile.js
. The file will be read and the console should print the poem back to you. If not, it will print out the error that was encountered, for example, if there is no such file at the specified path.
Callbacks are suitable for one-time handling of data, errors, and events. However, callbacks can get complicated when they are nested several levels deep. Another alternative way of handling events is to use Event Listeners, which are covered in the next section.
Responding to Events with Event Listeners
Event listeners are functions that get to run when specific event types occur. For example, when reading a file, making a server connection or querying a database, the modules that we make use of, such as fs
, net
, or mongoose
, all have built-in event types that they will emit.
The objects that typically emit these events extend the base EventEmitter
object, which comes from the built-in events module.
Your application can respond to these events through the mechanism of event listeners. Typically, you attach an event listener in code through the means of the keyword "on", followed by a string specifying the event type, and then finally a function, which is the code to run when the event occurs.
To see event listeners in action, we are going to create a server that interacts with a Cat API and parse responses from the API. Our server will then serve requests and show visitors a "Cat of the Day" image. The events we will be working with are part of the http
module.
We will also be using a xml2js module to parse the XML responses that the Cat API produces. To install xml2js
, you will want to run the command npm install xml2js
in a suitable project directory.
Once you have installed the module, create two files in the directory, cats.html
, and cats.js
. Inside cats.html
, place the front-end of our application. This will simply display the cat data we are going to be parsing.
<!-- cats.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cats</title>
<!-- Bootstrap -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container-fluid">
<div class="col-md-8 col-md-offset-2">
<h1>Cats Of Silicon Valley</h1>
<h2>Welcome to the Cat Of The Day</h2>
<img src=IMGSRC class="img-fluid" alt="Responsive image">
<br>
<label class="primary">Source: SOURCE</label>
<br>
<a href="/" class="btn btn-primary btn-lg">More Cats!</a>
</div>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>
The bulk of our logic is going to be in the server-side code that works with the event listeners. This is contained in the cats.js
file. To see the event listener code in action, place the following code inside the file, then run it with node cats.js
, and, in your browser, visit http://localhost:4000
.
// cat.js
const http = require('http');
const fs = require('fs');
const xml2js = require('xml2js');
// We will get images from the CatAPI https://thecatapi.com/
let catApi = 'http://thecatapi.com/api/images/get?format=xml&results_per_page=1';
let catUrl = '';
let catSource = '';
let server = http.createServer(function(req, res) {
// Get fresh cat data from the Cat API
http.get(catApi, (res) => {
let data = '';
// Attach event listener for when receiving data from the remote server is complete
res.on('end', () => {
console.log('***We have completed cat data\n***');
console.log(data);
let parser = new xml2js.Parser();
return parser.parseString(data, function(err, imgxml) {
if (err) {
return console.log('Error parsing cat data');
} else {
let imgjson = JSON.parse(JSON.stringify(imgxml));
console.log('***We have cat JSON***');
console.log(imgjson);
catUrl = imgjson.response.data[0].images[0].image[0].url[0];
return catSource = imgjson.response.data[0].images[0].image[0].source_url[0];
}
});
});
// Event listener for the 'data' event
// In this case, accumulate all the data so we can use it all at once later
return res.on('data', (xml) => {
return data += xml;
});
});
// Serve cat images from the CatAPI
return fs.readFile('./cats.html', function(err, cathtml) {
if (err) {
console.error(err);
return res.end('An error occurred');
}
let html = cathtml.toString()
.replace('IMGSRC', catUrl)
.replace('SOURCE', catSource);
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.write(html);
return res.end();
});
});
// Run the server
server.listen(4000);
Below, we go into the code in detail. Also take a look at the comments in the code.
As you can see from the code, our request to the Cat API requests new cat data. We then let Node.js execution continue as normal. However, we attach two event listeners to deal with new events from the remote API. The first of these is an "on end" event listener. When we have a complete cat payload from the Cat API, we then update our page with the new data and image. The second class of event we are listening for is the "data" event. This is triggered when there is new data from the remote host. In that case, we buffer up the data and add it to our temporary data store.
Now, thanks to the power of event listeners, we've made it easy to get new cat images at will.
Our website visitors can get new Cat of the Day images at the click of a button.
There's a lot more to events and timers in Node.js than what we described here. A good next topic to explore is that of event emitters, which give you even more power over the kinds of events your application can make use of.