Don't Unleash Zalgo in your Node.js application.
That's my sincere advice.
And I believe that you can easily avoid the traps of Zalgo while building your next API. But for that, you need to understand how it looks like.
In the real world, the term Zalgo comes from a meme. It happens to be an Internet legend about an ominous entity believed to cause insanity, death and destruction of the world. Zalgo is often associated with scrambled text on web pages and photos of people whose eyes and mouths have been covered in black.
In the matrix-world of software development, Zalgo is a piece of code that can destroy your application.
And you won't even know what's going on.
The First Signs of Zalgo - Confused Code
Callbacks make it possible for JavaScript to handle concurrency despite being single-threaded (technically).
This is what essentially drives concurrency in Node.js.
You can build callbacks that behave synchronously or asynchronously. Typically, you choose only one approach.
The problem shows up when you end up choosing both - sync and async.
In other words, what if you develop a function that runs synchronously in certain conditions and asynchronously in some other conditions?
It's an unpredictable function.
Unpredictable functions and the APIs built using them unleash Zalgo in your code.
Example
Here's an example:
let cache = {};
function getStringLength(text, callback) {
if (cache[text]) {
callback(cache[text])
} else {
setTimeout(function() {
cache[text] = text.length;
callback(text.length)
}, 1000)
}
}
Despite the innocent appearance, the getStringLength()
function is evil.
Though it is used to simply calculate the length of a string, it has two faces.
If the string and its length are available in the cache
object, the function behaves synchronously by returning the data right away from the cache.
Otherwise, it calculates the length of the string and stores the result in the cache
before triggering the callback. The calculation happens asynchronously using a setTimeout()
.
Do note that the use of setTimeout()
is to force an asynchronous behaviour. You can replace it with any other asynchronous activity such as reading a file or making an API call. The idea is to demonstrate that a function can have different behaviour in different situations.
“But how does it unleash Zalgo?” you may ask.
Let us write some more logic to actually use this unpredictable function. Check it out below:
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
let cache = {};
function getStringLength(text, callback) {
if (cache[text]) {
callback(cache[text])
} else {
setTimeout(function() {
cache[text] = text.length;
callback(text.length)
}, 1000)
}
}
function determineStringLength(text) {
let listeners = []
getStringLength(text, function(value) {
listeners.forEach(function(listener) {
listener(value)
})
})
return {
onDataReady: function(listener) {
listeners.push(listener)
}
}
}
async function testLogic() {
let text1 = determineStringLength("hello");
text1.onDataReady(function(data) {
console.log("Text1 Length of string: " + data)
})
await sleep(2000);
let text2 = determineStringLength("hello");
text2.onDataReady(function(data) {
console.log("Text2 Length of string: " + data)
})
}
testLogic();
Pay special attention to the determineStringLength()
function. It is a sort of wrapper around the getStringLength()
function.
The determineStringLength()
function creates a new object that acts as a notifier for the string length calculation. When the string length is calculated by the getStringLength()
function, the listener functions registered within determineStringLength()
get invoked.
To test this concept, you have the testLogic()
function at the very end end.
The test function calls determineStringLength()
function twice for the same input string “hello”. Between the two calls, we pause the execution for a couple of seconds using the sleep()
function. This is just to introduce a bit of time lag between the two calls.
Running the program provides the below result:
Text1 Length of string: 5
Only one statement is printed. The callback for the second operation never got invoked.
- For
text1
, thegetStringLength()
function behaves asynchronously since the data is not available in the cache. Therefore, the listener got registered and the output was printed. - For
text2
, none of this happens. It gets created in an event loop cycle that already has the data available in the cache. Therefore,getStringLength()
behaves synchronously and the callback that's passed to it gets called immediately. This in turn calls all the registered listeners synchronously. However, registration of new listener happens later and hence it is never invoked.
The root of this problem is the unpredictable nature of getStringLength()
function. Instead of providing consistency, it increases the unpredictability of our program.
Such bugs can turn out to be extremely complicated to identify and reproduce in a real application. More often, they cause nasty bugs and unleash Zalgo in our application.
So, how can we avoid all of this mess?
Use Deferred Execution
It might appear tricky but preventing such situations can be quite simple. Just make sure the functions you are writing behave consistently in terms of synchronous vs asynchronous behaviour.
For example, check the below code:
function getStringLength(text, callback) {
if (cache[text]) {
process.nextTick(function() {
callback(cache[text]);
});
//callback(cache[text])
} else {
setTimeout(function() {
cache[text] = text.length;
callback(text.length)
}, 1000)
}
}
Instead of directly triggering the callback, you can wrap it inside the process.nextTick()
. This defers the execution of the function until the beginning of the next event loop phase.
Subtle reasons can cause nasty bugs. Unleashing Zalgo is one of them and hence, an interesting name was given to this situation.
As I mentioned in the beginning, the term was first used by Isaac Z. Schlueter who was also inspired by a post on Havoc’s Blog. Below are the links to those blogs:
Designing APIs for Asynchrony
Callbacks, synchronous and asynchronous
You can check out those posts to get more background. I hope the example in this post was useful in understanding the issue on a more practical level.
If you liked this post, consider sharing it with friends and colleagues. You can also connect with me on other platforms.
Top comments (0)