Asynchronous Processing in JavaScript Part 1
- Duong Hoang
- Jun 24, 2024
- 5 min read
Asynchronous Processing in JavaScript
Asynchronous programming is a fundamental concept in JavaScript, but not everyone understands it thoroughly; most people just follow habits. I have worked on several maintenance projects and noticed many errors in handling asynchronous processes, leading to slow program execution or incorrect logic. At the beginning of a project, these issues might not be noticeable, but as the data grows, it can cause stuttering, lag, or hidden bugs.
I wrote this article to share my knowledge and experience in handling asynchronous processes in JavaScript, hoping to help you avoid the mentioned errors.

JavaScript Theory
First, let's review some basic theory about asynchronous processes in JavaScript.
Typically, when writing applications in JavaScript, we execute functions sequentially, from top to bottom, like this:
func1();
func2();
func3();
This is called synchronous processing (sync). This kind of JavaScript code is simple and easy to read, but in many cases, writing this way can slow down the program.
We can write JavaScript differently so that these functions run in parallel, not sequentially, and run simultaneously. This way, the program's speed will be faster, and the functions won't need to wait for each other (blocking). This is called asynchronous processing (async) in JavaScript.
Asynchronous programming in JavaScript is more challenging because although the function calls are in the order of func1, func2, func3, they run simultaneously, so func3 might finish before func2, affecting the logic flow.
In JavaScript, some built-in functions are either sync or async. For example, string and number processing functions are sync: toUpperCase(), substr(), etc. Callback functions are async: setTimeout(), fetch(), etc.
In NodeJS, you can see the same task has two functions like writing to a file: fs.writeFile (async) and fs.writeFileSync (sync). The async function is recommended because it doesn't block the program like sync. For example, if file read/write operations fail or take too long, the program will be blocked, waiting for this process to complete before executing subsequent tasks.
Ok, let's keep the theory to this point. For deeper theory like blocking, non-blocking, event loop, etc., please research further. Too much theory can be boring, so let's move on to practical examples in JavaScript.
JavaScript Examples
Below are some JavaScript functions used in the examples:
setTimeout: A built-in JavaScript function that runs asynchronously, used to delay the execution of a function after a certain period.
This is a custom JavaScript function to replace setTimeout but runs synchronously, simulating the execution time of a function by waiting for x seconds.
function delay(x) {
const start = new Date().getTime();
while (new Date().getTime() - start < x * 1000) {}
}
console.time(): To measure the program's execution time in JavaScript.
Ok, now let's get to the real example. I will write a JavaScript program that simulates the process of boiling vegetables, because probably everyone knows how to boil vegetables 😂.
We have functions describing actions like this:
function prepareVegetables() {
delay(3);
console.log('Preparing vegetables.');
}
function boilWater() {
delay(4);
console.log('Boiling water.');
}
function boilVegetables() {
delay(5);
console.log('Boiling vegetables.');
}
function removeVegetables() {
delay(3);
console.log('Removing vegetables, cooling them.');
}
Each JavaScript function is simulated to take a few seconds (in reality, each step might take 5-10 minutes).
Execute the JavaScript program:
console.time('Total time');
prepareVegetables();
boilWater();
boilVegetables();
removeVegetables();
console.timeEnd('Total time');
The total time will be approximately 15 seconds (3+4+5+3). You can copy the JavaScript code above and run it in the browser's console to test.
In reality, if you boil vegetables this way, you might get scolded for not knowing how to manage your time 😅.
To optimize time, I'll switch to doing multiple tasks simultaneously in JavaScript. Rewrite the above program asynchronously using setTimeout and callbacks:
function prepareVegetables(callback) {
setTimeout(() => {
console.log('Preparing vegetables.');
if (callback) callback();
}, 3000);
}
function boilWater(callback) {
setTimeout(() => {
console.log('Boiling water.');
if (callback) callback();
}, 4000);
}
function boilVegetables(callback) {
setTimeout(() => {
console.log('Boiling vegetables.');
if (callback) callback();
}, 5000);
}
function removeVegetables(callback) {
setTimeout(() => {
console.log('Removing vegetables, cooling them.');
if (callback) callback();
}, 3000);
}
These JavaScript functions are rewritten with callbacks, allowing a function to be passed as a parameter and called back after executing some logic. Above, I named the callback function callback, but you can name it according to the context.
Now these JavaScript functions run asynchronously, so if we call them like this:
console.time('run');
prepareVegetables();
boilWater();
boilVegetables();
removeVegetables();
console.timeEnd('run');
The result will be like this: Execution result 1
console.timeEnd runs before the other console.log statements, and the steps are in a random order, such as removing vegetables before the water even boils 😂.
This is because these JavaScript functions run simultaneously without depending on each other, leading to undesired results. In reality, we can optimize the vegetable boiling process by preparing the vegetables and boiling water simultaneously, but we still need to wait for the water to boil before boiling the vegetables and wait for the vegetables to boil before removing them.
So, I'll rewrite the JavaScript program to run prepareVegetables() and boilWater() simultaneously. After both functions finish, I'll call boilVegetables() and removeVegetables() sequentially:
console.time('run');
// Add a counter to check the number of callbacks executed
let count = 0;
// This function checks when 2 callbacks are executed (prepareVegetables() and boilWater())
function checkCallback() {
count++;
// If 2 callbacks are executed, continue with the next functions
if (count === 2) {
// Call boilVegetables() with a callback to call removeVegetables() after boiling
boilVegetables(() => {
removeVegetables(() => {
// After removing vegetables, end the program
console.timeEnd('run');
});
});
}
}
// Execute prepareVegetables and boilWater simultaneously with checkCallback as the callback
prepareVegetables(checkCallback);
boilWater(checkCallback);
Execution result 2
The JavaScript functions still run in the desired order, and we have reduced the total time to about 12 seconds, saving 3 seconds.
In reality, if there are many functions, performance will be significantly improved compared to sequential calls. For example, on a screen needing many API calls, if you habitually use async/await and await each API call sequentially, that's essentially synchronous programming in JavaScript, making the web page load very slowly.
Asynchronous programming in JavaScript also has the drawback of being harder to manage, especially if we only use callbacks as in the example above, leading to callback hell, making the code hard to read and maintain. Try increasing the complexity of the above example, and you'll see more clearly the challenge of callback hell:
Rewrite the vegetable boiling program above, but split the prepareVegetables step into pluckVegetables and washVegetables. We need pluckVegetables(), washVegetables(), and boilWater() to run simultaneously but still ensure pluckVegetables() runs before washVegetables().
Callback Hell
Now it's time for you to practice to understand more about asynchronous programming with callbacks in JavaScript. In the next part, I will guide you on asynchronous programming with Promises, async/await, and ways to handle errors to ensure the program runs correctly in JavaScript.
Comentarios