[Node.js] Why using sync versions of async functions is bad.

[Node.js] Why using sync versions of async functions is bad.

ยท

4 min read

Hey!

My name is Gaurang and I've been working with Node.js for the past 5 years. This article is part of the "My Guide to Getting through the Maze of Callbacks and Promises" series. It's a 3-part series where I talk about writing asynchronous code with Node.js.

The articles are intermediate-level and it is expected that the reader already has basic familiarity with Node.js syntax and the constructs related to asynchronous programming in it. Such as callbacks, promises, and async-await.


Sync of Async what?

Before I go ahead and beef about synchronous functions, I suppose I should briefly describe synchronous as well as asynchronous functions.

Many Node.js packages provide synchronous alternatives to their asynchronous functions. For example, the fs package has fs.readFile (which works asynchronously) and fs.readFileSync (which works synchronously).

The working of fs.readFileSync is similar to traditional functions. Your program calls the method and waits for the file to be read. Once the file is completely read, it returns the contents. The program then resumes execution of the rest of the code.

In contrast, when our program calls the asynchronous fs.readFile method, it does not wait for the method to read the entire file and return the contents. Our program moves ahead with the execution of the rest of the code immediately; while fs.readFile continues with the reading task in parallel.

If there's any code you want to execute after the file contents have been read, you need to wrap the code in a function and pass that function to fs.readFile as a callback. fs.readFile will call the callback function as soon as the file contents have been read. This way of writing code is an engineering choice made by the creators of Node.js. And it allows Node.js to be extremely fast despite being single-threaded.

Here's a sample script demonstrating the difference between synchronous and asynchronous programming in JavaScript.

What's wrong with using sync versions of async functions?

Today, I often come across codebases where callbacks, promises, and async-await have been mixed together. Turning the code into a hotch-potch. I sympathize with the people that have to work with such codebases. Understanding callbacks and promises is difficult. Writing code with callbacks is difficult. Understanding the difference between synchronous and asynchronous functions is difficult.

The advent of async-await has made writing code in JavaScript fairly straightforward. Most people learning JavaScript today do not know what a pain it used to be when using callbacks was the primary method of writing code. As a result, they often lack experience in handling callbacks.

People often retreat to synchronous versions of library methods because they're more convenient. E.g. fs.readFileSync, fs.writeFileSync, fs.copyFileSync, child_process.execSync, etc. In my opinion, that is one of the worst things to do to your project.

People generally opt for Node.js due to its reputation for fast network and I/O operations. However, it's crucial not to overlook the fundamental reason behind this efficiency: Node.js's single-threaded, asynchronous execution model! Utilizing synchronous versions of I/O and network operations can significantly impair your application's performance compared to counterparts developed in alternative languages.

Asynchronous functions are designed to be non-blocking and to avoid delays. As Node.js is single-threaded, just one long-running synchronous (blocking) task is enough to degrade the performance of the entire application. Using synchronous versions of asynchronous functions may seem like an easy solution at first, but it is the devil in disguise. It can cause delays all over the application; leading to frustration for developers and users alike.

Of course, if you're writing a small independent one-time script, the performance degradation may not matter much. But in large production-level applications, this is going to cause unnecessary bottlenecks.

Here's a short demo of how a synchronous method call affects the rest of the application.

This script starts a barebones Node.js server that has three APIs.

  • GET /quick-api

  • GET /slow-sync-api

  • GET /slow-async-api

The quick-api API is supposed to return a response immediately. But, if you call quick-api after calling slow-sync-api, it does not return until slow-sync-api returns. This is because Node.js is single-threaded. And slow-sync-api calls a synchronous method that does not let the main thread do anything else until its execution is complete. It blocks all other requests. However, if you call quick-api after slow-async-api, the quick-api works as expected. This is because slow-async-api does not block the main thread till its execution is complete. The work of the asynchronous function is deferred to a worker thread and the main thread waits for the execution to complete. Meanwhile, the main thread is free to fulfill other requests.

Conclusion

Asynchronous execution is integral to JavaScript. So, you'll find asynchronous functions everywhere. Transitioning from a traditional synchronous mindset to an asynchronous one takes some adjustment.

However, using the synchronous versions of asynchronous functions is a bad practice. It can lead to unexpected behavior and performance bottlenecks. When performing asynchronous operations, it's best to use the asynchronous version of functions whenever possible. This will help you avoid any potential problems and ensure that your code is as efficient as possible.


I've put a lot of work into this article. And I hope that it has been helpful. If you have any questions, please feel free to leave a comment below. I'd really appreciate it if you could like and share the article! ๐Ÿ˜Š

Did you find this article valuable?

Support Gaurang Pansare by becoming a sponsor. Any amount is appreciated!

ย