It’s 3 a.m. You’ve got your hands over the keyboard, staring at an empty console. The bright prompt over a dark backdrop is ready, yearning to take in commands. Want to hack up Node.js for a little while?
One exciting thing about Node.js is that it runs anywhere. This opens up various possibilities for experimenting with the stack. For any seasoned veteran, this is a fun run of the command line tooling. What’s extra special is that we can survey the stack from within the safety net of the command line. And it’s cool that we’re still talking about JavaScript — so most readers who are familiar with JS shouldn’t have any problem understanding how it all works. So, why not fire up node
up in the console?
In this article, we’ll introduce you to Node.js. Our goal is to go over the main highlights while hiking up some pretty high ground. This is an intermediate overview of the stack while keeping it all inside the console. If you want a beginner-friendly guide to Node.js, check out SitePoint’s Build a Simple Back-end Project with Node.js course.
Why Node.js?
Before we begin, let’s go over the tidbits that make Node.js stand out from the crowd:
- it’s designed for non-blocking I/O
- it’s designed for asynchronous operations
- it runs on Chrome’s V8 JavaScript engine.
You may have heard these points through many sources, but what does it all mean? You can think of Node.js as the engine that exposes many APIs to the JavaScript language. In traditional computing, where processes are synchronous, the API waits before it runs the next line of code when you perform any I/O operation. An I/O operation is, for example, reading a file or making a network call. Node.js doesn’t do that; it’s designed from the beginning to have asynchronous operations. In today’s computing market, this has a tremendous advantage. Can you think of the last time you bought a new computer because it had a faster single processor? The number of cores and a faster hard drive is more important.
In the remainder of this article, when you see a >
, which is a prompt symbol, it means you should hit Enter to type up the next command. Moreover, before running the code in this article, you have to open the CLI and execute the command node
. With that said, let’s begin our tour!
Callbacks
To start, type up this function:
> function add(a, b, callback) { var result = a + b; callback(result); }
To a newbie, a callback in JavaScript may seem strange. It certainly doesn’t look like any classical OOP approach. In JavaScript, functions are objects and objects can take in other objects as parameters. JavaScript doesn’t care what an object has, so it follows that a function can take in an object that happens to be yet another function. The arity, which is the number of parameters, goes from two in add()
to a single parameter in the callback. This system of callbacks is powerful, since it enables encapsulation and implementation hiding.
In Node.js, you’ll find a lot of APIs that take in a callback as a parameter. One way to think about callbacks is as a delegate. Programming lingo aside, a delegate is a person sent and authorized to represent others. So a callback is like sending someone to run an errand. Given a list of parameters, like a grocery list for example, they can go and do a task on their own.
To play around with add
:
> add(2, 3, function (c) { console.log('2 + 3 = ' + c) });
> add(1, 1, function (c) { console.log('Is 1 + 1 = 3? ' + (c === 3)); });
There are plenty more creative ways to play around with callbacks. Callbacks are the building blocks for some important APIs in Node.js.
Asynchronous Operations
With callbacks, we’re able to start building asynchronous APIs. For example:
> function doSomething (asyncCallback) { asyncCallback(); }
> doSomething(function () { console.log('This runs synchronously.'); });
This particular example has a synchronous execution. But we have everything we need for asynchronicity in JavaScript. The asyncCallback
, for example, can get delayed in the same thread:
> function doSomething (asyncCallback) { setTimeout(asyncCallback, Math.random() + 1000); }
> doSomething(function () { console.log('This runs asynchronously.'); }); console.log('test');
We use a setTimeout
to delay execution in the current thread. Timeouts don’t guarantee time of execution. We place a Math.random()
to make it even more fickle, and call doSomething()
, followed by a console.log('test')
, to display delayed execution. You’ll experience a short delay between one to two seconds, then see a message pop up on the screen. This illustrates that asynchronous callbacks are unpredictable. Node.js places this callback in a scheduler and continues on its merry way. When the timer fires, Node.js picks up right where execution happens to be and calls the callback. So, you must wrap your mind around petulant callbacks to understand Node.js.
In short, callbacks aren’t always what they seem in JavaScript.
Let’s go on with something cooler — like a simple DNS lookup in Node.js:
> dns.lookup('bing.com', function (err, address, family) { console.log(' Address: ' + address + ', Family: ' + family + ', Err: ' + err); });
The callback returns err
, address
, and family
objects. What’s important is that return values get passed in as parameters to the callback. So this isn’t like your traditional API of var result = fn('bing.com');
. In Node.js, you must get callbacks and asynchrony to get the big picture. (Check out the DNS Node.js API for more specifics.) This is what DNS lookupc can look like in a console:
File I/O
Now let’s pick up the pace and do file I/O on Node.js. Imagine this scenario where you open a file, read it and then write content into it. In modern computer architecture, I/O-bound operations lag. CPU registers are fast, the CPU cache is fast, RAM is fast. But you go read and write to disk and it gets slow. So when a synchronous program performs I/O-bound operations, it runs slowly. The better alternative is to do it asynchronously, like so:
> var fs = require('fs');
> fs.writeFile('message.txt', 'Hello Node.js', function () { console.log('Saved.'); }); console.log('Writing file...');
Because the operation is asynchronous, you’ll see “Writing file…” before the file gets saved on disk. The natural use of callback functions fits well in this API. How about reading from this file? Can you guess off the top of your head how to do that in Node.js? We’ll give you a hint: the callback takes in err
and data
. Give it a try.
Here’s the answer:
> fs.readFile('message.txt', function(err, data) { console.log(data); });
You may also pass in an encoding
option to get the utf-8
contents of the file:
> fs.readFile('message.txt', {encoding: 'utf-8'}, function(err, data) { console.log(data); });
The use of callback functions with async I/O looks nice in Node.js. The advantage here is that we’re leveraging a basic building block in JavaScript. Callbacks get lifted to a new level of pure awesomeness with asynchronous APIs that don’t block.
A Web Server
So, how about a web server? Any good exposé of Node.js must run a web server. Imagine an API named createServer
with a callback that takes in request
and response
. You can explore the HTTP API in the documentation. Can you think of what that looks like? You’ll need the http
module. Go ahead and start typing in the console.
Here’s the answer:
> var http = require('http');
> var server = http.createServer(function (request, response) { response.end('Hello Node.js'); });
The Web is based on a client-server model of requests and responses. Node.js has a request
object that comes from the client and a response
object from the server. So the stack embraces the crux of the Web with this simple callback mechanism. And of course, it’s asynchronous. What we’re doing here is not so different from the file API. We bring in a module, tell it to do something and pass in a callback. The callback works like a delegate that does a specific task given a list of parameters.
Of course, everything is nonsense if we can’t see it in a browser. To fix this, type the following in the command line:
server.listen(8080);
Point your favorite browser to localhost:8080
, which in my case was Edge.
Imagine the request
object as having a ton of information available to you. To rewire the server
, let’s bring it down first:
> server.close();
> server = http.createServer(function (request, response) { response.end(request.headers['user-agent']); }); server.listen(8081);
Point the browser to localhost:8081
. The headers
object gives you user-agent
information which comes from the browser. We can also loop through the headers
object:
> server.close();
> server = http.createServer(function (request, response) { Object.keys(request.headers).forEach(function (key) { response.write(key + ': ' + request.headers[key] + ' '); }); response.end(); }); server.listen(8082);
Point the browser to localhost:8082
this time. Once you’ve finished playing around with your server, be sure to bring it down. The command line might start acting funny if you don’t:
> server.close();
So there you have it, creating web servers all through the command line. I hope you’ve enjoyed this psychedelic trip around node
.
Async Await
ES 2017 introduced asynchronous functions. Async functions are essentially a cleaner way to work with asynchronous code in JavaScript. Async/Await was created to simplify the process of working with and writing chained promises. You’ve probably experienced how unreadable chained code can become.
Creating an async
function is quite simple. You just need to add the async keyword prior to the function:
async function sum(a,b) {
return a + b;
}
Let’s talk about await
. We can use await
if we want to force the rest of the code to wait until that Promise resolves and returns a result. Await only works with Promises; it doesn’t work with callbacks. In addition, await
can only be used within an async
function.
Consider the code below, which uses a Promise to return a new value after one second:
function tripleAfter1Second(number) {
return new Promise(resolve => {
setTimeout(() => {
resolve(number * 3);
}, 1000);
});
}
When using then
, our code would look like this:
tripleAfter1Second(10).then((result) => {
console.log(result); // 30
}
Next, we want to use async/await. We want to force our code to wait for the tripled value before doing any other actions with this result. Without the await
keyword in the following example, we’d get an error telling us it’s not possible to take the modulus of ‘undefined’ because we don’t have our tripled value yet:
const finalResult = async function(number) {
let triple = await tripleAfter1Second(number);
return triple % 2;
}
One last remark on async/await: watch out for uncaught errors. When using a then
chain, we could end it with catch
to catch any errors occurring during the execution. However, await doesn’t provide this. To make sure you catch all errors, it’s a good practice to surround your await statement with a try … catch
block:
const tripleResult = async function(number) {
try {
return await tripleAfter1Second(number);
} catch (error) {
console.log("Something wrong: ", error);
}
}
For a more in-depth look at async/await, check out Simplifying Asynchronous Coding with Async Functions.
Conclusion
Node.js fits well in modern solutions because it’s simple and lightweight. It takes advantage of modern hardware with its non-blocking design. It embraces the client-server model that’s intrinsic to the Web. Best of all, it runs JavaScript — which is the language we love.
It’s appealing that the crux of the stack is not so new. From its infancy, the Web got built around lightweight, accessible modules. When you have time, make sure to read Tim Berners-Lee’s Design Principles. The principle of least power applies to Node.js, given the choice to use JavaScript.
Hopefully you’ve enjoyed this look at command line tooling. Happy hacking!
This article was peer reviewed by Rabi Kiran. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!