[Node.js] Why using sync versions of async functions is bad.
Tags: javascript, node, programming, performance
Published: 2023-03-05 15:08:14

Hi!
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.

---

## Table of Contents
- [Sync of Async what?](#sync-async-what)
- [What's wrong with using sync versions of async functions?](#why-sync-wrong)
- [Conclusion](#conclusion)

## Sync of Async what? <a name="sync-async-what" \>
Before I go ahead and beef about synchronous functions, I should briefly describe synchronous as well as asynchronous functions.

Many Node.js packages provide synchronous alternatives of their asynchronous functions.
For example, the `fs` package has [`fs.readFile`][readfile] (which works asynchronously) and [`fs.readFileSync`][readfile-sync] (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.

Now reading a file is a slow I/O operation that could take  a few seconds.
So 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 function.
`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 JavaScript/Node.js that allows it to be extremely fast despite being single-threaded.

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

{% gist https://gist.github.com/gaurang847/2cfc5a2b0edda0db1950a289cb4baafa file=sync_vs_async.js %}

## What's wrong with using sync versions of async functions? <a name="why-sync-wrong" \>
I often come across codebases where callbacks, promises, and async-await have been mixed together. Turning the code into a hotchpotch.
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][async-await] has made writing code in JavaScript fairly straightforward. And so, 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`][readfile-sync], `fs.writeFileSync`, `fs.copyFileSync`, `child_process.execSync`, etc.
In my opinion 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, it 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.

{% gist https://gist.github.com/gaurang847/38e6f62eff32ac2e3bfb444f74f13585 file=sync_seriousness_demo.js %}

{% embed https://youtu.be/xxKffpI7EIY %}

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 till 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 fulfil other requests.

## Conclusion <a name="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! 😊

[async-await]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
[readfile]: https://nodejs.org/api/fs.html#fsreadfilepath-options-callback
[readfile-sync]: https://nodejs.org/api/fs.html#fsreadfilesyncpath-options