Asynchronous code in JavaScript can be a headache, especially if you are just starting to work with it. Modern browsers and server environments, such as Node.js, actively use asynchronicity to avoid blocking code execution while waiting for a response from the server, reading a file, or executing a database query.
Callbacks and Their Problems
Previously, asynchronicity in JavaScript was implemented through callbacks (callback functions). These are functions that are passed as arguments and are called after the asynchronous operation is completed.
function fetchData(callback) {
setTimeout(() => {
callback("Data received!");
}, 1000);
}
fetchData((message) => {
console.log(message);
});
This approach works, but as the logic becomes more complex, it can turn into what is known as “callback hell”:
function step1(callback) {
setTimeout(() => {
console.log("Step 1");
callback();
}, 1000);
}
function step2(callback) {
setTimeout(() => {
console.log("Step 2");
callback();
}, 1000);
}
function step3(callback) {
setTimeout(() => {
console.log("Step 3");
callback();
}, 1000);
}
step1(() => {
step2(() => {
step3(() => {
console.log("Completed!");
});
});
});
This code is hard to read, maintain, and extend. Therefore, Promise was introduced.
Promises: A New Level of Asynchronicity
The Promise object allows you to work with asynchronous operations in a more structured way.
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data received!");
}, 1000);
});
}
fetchData().then((message) => {
console.log(message);
});