Asynchronous JavaScript Coding

Getting into the Node Mindset Published by Nathan A. Wilcox on July 22, 2020 and updated on July 23, 2020

Who can benefit from this?

This article is for those who have some experience in web development, but would like brief introduction in how to write code for a Node.js application. Even if you are a seasoned veteran, if you come away from this with only a single new piece of information, it was worth the time. 

Node is quickly becoming one of the most popular platforms due to its versatility and the eco system of modules and extensions supported by an active community, so knowing how to write code for it will be a very useful skill to have in your tool belt.


Getting our head right

Working effectively with node requires a certain mindset. Typically when we start learning a new technology or language, we try to find familiar ways of doing things. For example, most languages are top down, executing one line at a time, waiting for it to finish, and then moving to the next line. We could jump into Node and write all our applications like this. However, this synchronous approach to code execution will not play well with the node style. It works fine in a multi threaded environment where each request can have its own thread to execute on, but node runs on a single thread, so to get the most out of it, we need to code in an asynchronous manner. We need to design our code in a continuation passing style (CPS) and leverage something called an event demultiplexer, which allows us to push off expensive work being done by a method and return execution back to the caller, freeing it up our application to handle new incoming requests. When using this approach, we are not hindered by the single threaded nature of node. 

To illustrate this concept, we are going to see how we can write a method that sends messages to distant planets, but does not wait for a response. If we had our only thread busy-wait every time we sent a request, our app would be all but useless. Our thread's time is very valuable, and it is up to us to make sure it stays as busy as possible with minimal idle time. But first, some conventions and terms to learn.


Some Node Styles and Conventions when writing  your API

What is CPS? It is a style of coding, where instead of returning a value from a method, you can pass a callback as a parameter, and the results/errors of the method are piped into the callback as parameters. This allows the method to return execution back to the application immediately, and when the expensive operation is complete, the callback will be invoked and the results will be available as parameters. This is the style that enables node applications to be asynchronous operating on a single thread. In addition to this, we have the added benefit of JavaScript closures, but going over closures is not within the scope of this article. Suffice to say, the callback's scope includes any variables visible to it during its creation. 

Now that we have vague concept of how we will structure our code, lets cover a couple conventions we will be following. Following these will make things easier for the developers reading and modifying your code.

  1. Callbacks are the last parameter in a method call.
  2. Errors come first when writing a handler.
  3. Pick either asynchronous or synchronous, and stick to it. A method call should not change behavior based on inputs. 
  4. Modules are included as constants to prevent accidental reassignment the references.


Learning By Doing (If it's not in your hands, it doesn't stay in your head)

Now its time to roll up our sleeve and write some code. For our example, we are going to write a CPS style method that will send a message to a far off place. Once the message is received, it will print a response to prove it was received. We will provide a list of destinations and iterate over them, calling our method for each destination. This simulates multiple web requests invoking an expensive operation. Keep in mind, with node we always want to avoid a single request blocking all subsequent requests.


let planets = ["Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]

function sendMessageTo(destination, callback) {
	
	//send message around the world
	let error = null;
	let result = `Hello from ${destination}`
	console.log(`Sending message to ${destination}`)
	
	callback(error, result);
}

console.log("Processing Requests")
planets.forEach(planet => sendMessageTo(planet, (err, res) => console.log(res)))
console.log("All Requests Processed")

We followed the conventions we talked about, so this looks like a node appropriate method. We designed out method to accept a callback, and pipe our results/errors to it. When we run this snippet, what do you think the output will be?


Processing Requests
Sending message to Mercury
Hello from Mercury
Sending message to Venus
Hello from Venus
Sending message to Mars
Hello from Mars
Sending message to Jupiter
Hello from Jupiter
Sending message to Saturn
Hello from Saturn
Sending message to Uranus
Hello from Uranus
Sending message to Neptune
Hello from Neptune
All Requests Processed

What went wrong? The above code is calling our send method for each destination, and waiting for all the responses before it processes the next send request. Can you see the problem this could cause for a single threaded web application? Sending messages to far off planets takes time, and if the application had to wait for all the responses before moving on a new request, the application would not be usable for weeks or months at a time. How can we solve this issue? Lets try by simulating the event demultiplexer using setTimeout().


let planets = ["Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]

function sendMessageTo(destination, callback) {
	
	//send message around the world
	let error = null;
	let result = `Hello from ${destination}`
	console.log(`Sending message to ${destination}`)
	
	setTimeout(function() {
	callback(error, result);
	},0);
}

console.log("Processing Requests")
planets.forEach(planet => sendMessageTo(planet, (err, res) => console.log(res)))
console.log("All Requests Processed")

In our updated method, we used the setTimeout() call to simulate pushing the expensive message operation to the event demultiplexer. When we do this, our sendMessageTo() method will return execution back to the application so future requests can be processed without waiting. Once the message is received at its destination, the callback is invoked with the results from the method, and the application can pick up where it left off. Let's analyze the output from our updated code.


Processing Requests
Sending message to Mercury
Sending message to Venus
Sending message to Mars
Sending message to Jupiter
Sending message to Saturn
Sending message to Uranus
Sending message to Neptune
All Requests Processed
Hello from Mercury
Hello from Venus
Hello from Mars
Hello from Jupiter
Hello from Saturn
Hello from Uranus
Hello from Neptune

This is much better. Our application will not sit idle while waiting for a response from each message that was sent. It will go back to doing what it does best, processing new requests. When the messages are received at the destination, the handler is invoked which gives the application the chance to continue the result. This approach avoids any busy-waiting on the application side, and makes the most out of the only thread we have.

Other ways to defer execution can be achieved through process.nextTick() or setImmediate(). They both give you the option to defer code execution in an asynchronous manner, but with subtle differences. process.nextTick() will be run before any I/O operations and setImmediate() will be queue behind any existing I/O events already in the queue. Use caution when using process.nextTick() since it always takes priority from I/O operations and can lead to I/O starvation.

Let's see how we can rewrite our method using these 2 options.


Using setImmediate()
let planets = ["Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]

function sendMessageTo(destination, callback) {
	
	//send message around the world
	let error = null;
	let result = `Hello from ${destination}`
	console.log(`Sending message to ${destination}`)

	setImmediate(function() {
		
		callback(error, result);
	});
}

console.log("Processing Requests")
planets.forEach(planet => sendMessageTo(planet, (err, res) => console.log(res)))
console.log("All Requests Processed")

Using process.nextTick()
const process = require('process')
let planets = ["Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]

function sendMessageTo(destination, callback) {
	
	//send message around the world
	let error = null;
	let result = `Hello from ${destination}`
	console.log(`Sending message to ${destination}`)

	process.nextTick(function() {
		
		callback(error, result);
	});
}
console.log("Processing Requests")
planets.forEach(planet => sendMessageTo(planet, (err, res) => console.log(res)))
console.log("All Requests Processed")

Note in the second example, we had to include the process module. Doing so is very simple. Since this is a core module, you will not need to install it using npm. You simple include the require('process') directive at the top of the script and assign a reference to the module. It is convention to always using const for modules to prevent accidentally reassigning the reference.

Both of these examples provide the same results as before, but are actually using the previously mentioned event demultiplexer to schedule the callbacks for future execution. This is the real node way of doing things.


Summary

In this article, we gave a brief intro is how to write code for a node application and why it is important to always consider how concurrent connections will share your thread. It is our job to make sure our thread stays as busy as possible. I/O operations like reading from the database or accepting user input take time, and we don't want other requests blocked because someone else can't decide if they want chicken or shrimp. We can simply register a callback that will be invoked when they make their selection, and allow our application to continue taking new requests. When we do this properly, the fact that node is single threaded moves away from being a burden, and becomes a real asset.


Comments



  • natewilcox commented on 7/22/2020, 2:22:29 PM

    I am new to blog writing, so any and all feedback is welcome. It was much more of a challenge than I anticipated to take thoughts from my head and write them down in a way that could be useful to others.