DEV Community

Cover image for Don't Unleash Zalgo in your Node.js Application!
Saurabh Dashora
Saurabh Dashora

Posted on • Originally published at progressivecoder.com

Don't Unleash Zalgo in your Node.js Application!

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.

meme-on-zalgo

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Only one statement is printed. The callback for the second operation never got invoked.

  • For text1, the getStringLength() 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

My Regular Newsletter

Twitter

LinkedIn

Youtube

Top comments (0)