Asynchronous JavaScript
In javascript we uptill now have done all the syncronous tasks, we print something, add 2 numbers and get our job done, but what if there is a huge operation that might take a lot of time to complete, how will we handle that case?
We either want to perform the rest of the code and let that work behind the scenes or we will need to wait for the completion of the heavy task and then perform the code.
There we have 2 jargons, 1) Blocking code and 2) Non - Blocking code
Because JavaScript is single-threaded, these long-running operations can block the execution of other code. To prevent this, we use asynchronous programming. This allows us to either:
Perform the rest of our code while the time consuming task runs in the background (non-blocking).
Or to wait for the completion of the time consuming task, but in a way that doesn't halt the execution of our program (asynchronous, but with a way to handle the result later).
This is achieved using techniques like callbacks, promises, and async/await, which allow us to write code that can handle asynchronous operations efficiently.
By default, JavaScript code executes in a blocking, synchronous manner.
Default Execution Model: JavaScript's execution model is single-threaded. This means that code is executed sequentially, one line at a time.
Blocking Behavior: Unless you explicitly use asynchronous techniques, each line of code must complete before the next line can execute. If a line of code takes a long time to complete (e.g., a complex calculation, a file read, or a network request), it will block the execution of all subsequent code.
Synchronous Nature: The sequential execution of code makes it synchronous. Each task is completed in the order it is encountered.
console.log("Start");
// A synchronous loop that takes some time
for (let i = 0; i < 1000000000; i++) {
// Some calculation
}
console.log("End");
How to Avoid Blocking:
To prevent blocking, you need to use asynchronous programming techniques:
Callbacks: Functions that are executed after an asynchronous operation completes.
Promises: Objects that represent the eventual completion (or failure) of an asynchronous operation.
Async/Await: Syntactic sugar built on top of promises that makes asynchronous code look more like synchronous code.
The Callback pattern of writing async operations
Before we had any of the promises in JS we used this callback method to perform async code. But this makes code very hard to read and an excess amount of writing goes into this.
// ------------- Legacy Code -------------
fs.readFile('./hello.txt', 'utf-8', function (err, content) {
if (err) {
console.log('Error in file reading', err);
} else {
console.log('File Reading Success', content);
fs.writeFile('backup.txt', content, function (err) {
if (err) {
console.log(`Error in writing backup.txt`, err);
} else {
fs.unlink('./hello.txt', function (e) {
if (e) {
console.log('Error deleteing file', e);
} else {
console.log('File delete success');
}
});
}
});
}
});
The Promises pattern of writing async operations
Here we are returned a new Promise for solving the async task, each promise will contain an error or resolution for the problem.
const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation here
// If successful, call resolve(value);
// If failed, call reject(error);
});
const fs = require('fs');
function readFileWithPromise(filepath, encoding) {
return new Promise((resolve, reject) => {
fs.readFile(filepath, encoding, (err, content) => {
if (err) {
reject(err);
} else {
resolve(content);
}
});
});
}
function writeFileWithPromise(filepath, content) {
return new Promise((resolve, reject) => {
fs.writeFile(filepath, content, function (err) {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
function unlinkWithPromise(filepath) {
return new Promise((resolve, reject) => {
fs.unlink(filepath, function (e) {
if (e) {
reject(e);
} else {
resolve();
}
});
});
}
The Async Await pattern of writing async operations
async function getUsers() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
console.log(users);
return users; // Returns a promise that resolves with the users data.
} catch (error) {
console.error("Error fetching users:", error);
throw error; // Rethrow the error to be caught by the caller.
}
}
// if resolver we use then
// otherwise we use catch in case of an error
getUsers()
.then((users) => {
console.log("Users processed:", users.length);
})
.catch((error) => {
console.error("Error in main:", error);
});
async function doTasks() {
try {
const fileContent = await readFileWithPromise('./hello.txt', 'utf-8'); // 1. Wait
console.log('All DOne'); // 2. Synchronous
await writeFileWithPromise('./backup.txt', fileContent); // 3. Wait
await wait(10); // 4. Wait
await unlinkWithPromise('./hello.txt'); // 5. Wait
throw new Error(''); // 6. Synchronous
} catch (e) {
console.log('Error', e); // 7. Synchronous
} finally {
console.log('All DOne'); // 8. Synchronous
}
}
console.log('Starting Program'); // 9. Synchronous
doTasks().then(() => console.log('All Done')); // 10. Asynchronous
console.log('End Of Program'); // 11. Synchronous
/*
Order of Execution:
"Starting Program"
"End Of Program"
(Asynchronously) readFileWithPromise starts, and the doTasks function pauses.
(After readFileWithPromise resolves) "All Done" (from inside doTasks)
writeFileWithPromise starts, and doTasks pauses.
(After writeFileWithPromise resolves) wait(10) starts, and doTasks pauses.
(After wait(10) resolves) unlinkWithPromise starts, and doTasks pauses.
(After unlinkWithPromise resolves) Error is thrown, and catch block executes.
"Error" (from inside catch)
"All Done" (from inside finally)
"All Done" (from the .then() callback)
*/