Synchronous vs. Asynchronous β Callbacks, Promises, async/await
After completing this topic:
You will be able to explain the difference between synchronous and asynchronous operations, and understand the evolution from callbacks to Promises to async/await.
Synchronous β One line at a time, in order
Synchronous execution means that the code runs line by line, from top to bottom. The next operation starts only after the previous one is completed.
console.log("1. Order received");
console.log("2. Making coffee (takes 3 minutes)");
console.log("3. Delivering coffee");
// Executes in the order 1 β 2 β 3It's intuitive, but it has a problem. While the coffee is being made, nothing else can happen. If reading a file takes 5 seconds, the server will be blocked for that time, and if a database query takes 2 seconds, other user requests won't be processed.
Asynchronous β Don't wait, move on
Asynchronous execution means that a time-consuming operation is started, and the code continues to the next line without waiting for it to finish. The result is received later when the operation is complete.
const fs = require("fs");
console.log("1. Starting file read");
// Asynchronous β Starts reading the file and immediately moves to the next line
fs.readFile("data.txt", "utf8", (err, data) => {
console.log("3. File read complete:", data);
});
console.log("2. Proceeding with the next task");
// Output order:
// 1. Starting file read
// 2. Proceeding with the next task β Executes first, without waiting!
// 3. File read complete: (file content)The output order is 1 β 2 β 3, not 1 β 3 β 2. If you just look at the numbers, it seems the same, but step 2 executes before step 3. This is because it doesn't wait for the file reading to finish.
Callbacks β The first approach to asynchronous operations
In the code above, (err, data) => { ... } is a callback function. It means "call this function when the operation is complete."
The problem arises when you need to perform multiple asynchronous operations in sequence:
// Callback hell
fs.readFile("a.txt", "utf8", (err, a) => {
fs.readFile("b.txt", "utf8", (err, b) => {
fs.readFile("c.txt", "utf8", (err, c) => {
console.log(a + b + c);
// The indentation keeps getting deeper...
});
});
});When callbacks are nested within each other like this, the code becomes difficult to read. This is called callback hell.
Promises β Solving callback hell
A Promise is a "promise to give a result later." You can chain them together using .then() to process operations in sequence without callback hell.
const fs = require("fs").promises;
fs.readFile("a.txt", "utf8")
.then(a => {
console.log("a read complete");
return fs.readFile("b.txt", "utf8");
})
.then(b => {
console.log("b read complete");
return fs.readFile("c.txt", "utf8");
})
.then(c => {
console.log("c read complete");
})
.catch(err => {
console.error("Error occurred:", err.message);
});The indentation is not excessive, and error handling is done in one place using .catch().
async/await β Making asynchronous code look synchronous
async/await is a syntax that makes Promises easier to write. Asynchronous code is written as if it were synchronous, reading from top to bottom.
const fs = require("fs").promises;
async function readAll() {
try {
const a = await fs.readFile("a.txt", "utf8");
const b = await fs.readFile("b.txt", "utf8");
const c = await fs.readFile("c.txt", "utf8");
console.log(a + b + c);
} catch (err) {
console.error("Error:", err.message);
}
}
readAll();await means "wait until this operation is complete," but it doesn't stop the entire program. Instead, it pauses only this function and continues other operations. You need to put async in front of the function to use await.
In modern JavaScript, almost everyone uses async/await. When asking AI for asynchronous code, using "write it with async/await" will give you the most readable code.
β Applied to bio: DevBench β Ajax and Fetch