Asynchronous programming allows JavaScript to execute tasks in a non-blocking manner.
This means that the program can continue executing other tasks while waiting for an operation (such as a network request or a file read operation) to complete.

Taken from: Medium
Asynchronous programming is essential for doing tasks that take time, such as:
- Fetching data from APIs.
- Handling user interactions.
- Executing time-based operations.
- Running tasks without freezing the UI in web applications.
That’s why JavaScript provides multiple ways to work with asynchrony:
- Callbacks
- Promises
- Async/Await
Callbacks
WARNING
Callbacks are the first approach to handle asynchrony in JavaScript. However, they can lead to nested callbacks, making the code hard to read and maintain.
A callback function is a function passed as an argument to another function, which is executed after an asynchronous operation completes.
// Example: Using a Callback in a Simulated API Request
function fetchData(callback) {
setTimeout(() => {
const data = "Data retrieved";
callback(data);
}, 2000);
}
fetchData((data) => {
console.log(data);
// Output: "Data retrieved"
// after 2 seconds
});
Callback Hell: A Common Problem
When multiple asynchronous operations rely on each other, nested callbacks can create complex and unreadable code, known as callback hell.
function step1(callback) {
setTimeout(() => {
console.log("Step 1 completed");
callback();
}, 1000);
}
function step2(callback) {
setTimeout(() => {
console.log("Step 2 completed");
callback();
}, 1000);
}
function step3(callback) {
setTimeout(() => {
console.log("Step 3 completed");
callback();
}, 1000);
}
step1(() => {
step2(() => {
step3(() => {
console.log("All steps completed");
});
});
});
This deeply nested structure is hard to manage and debug. Promises were introduced to solve this problem.
Promises: A Better Way to Handle Asynchrony
A promise represents a value that may be available now, in the future, or never. A promise has three possible states:
- Pending: The initial state, before the operation completes.
- Fulfilled: The operation completed successfully.
- Rejected: The operation failed.
Creating and Using a Promise
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve("Data fetched successfully");
} else {
reject("Error fetching data");
}
}, 2000);
});
myPromise
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
})
.finally(() => {
console.log("Operation completed");
});
Chaining Promises
Promises can be chained to execute multiple asynchronous operations sequentially.
function step1() {
return new Promise(resolve => {
setTimeout(() => {
console.log("Step 1 completed");
resolve();
}, 1000);
});
}
function step2() {
return new Promise(resolve => {
setTimeout(() => {
console.log("Step 2 completed");
resolve();
}, 1000);
});
}
function step3() {
return new Promise(resolve => {
setTimeout(() => {
console.log("Step 3 completed");
resolve();
}, 1000);
});
}
step1()
.then(step2)
.then(step3)
.then(() => {
console.log("All steps completed");
});
This structure is much more readable than nested callbacks.
Async/Await: Writing Cleaner Asynchronous Code
The async/await syntax, introduced in ES2017, makes working with promises more intuitive and readable.
Declaring an Async Function
An async
function always returns a promise. The await
keyword pauses execution until the promise resolves.
async function fetchData() {
return "Data retrieved";
}
fetchData().then(data => console.log(data));
Using Async/Await with Try-Catch
The try...catch
block handles errors in async functions.
async function fetchData() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Error fetching data:", error);
}
}
fetchData();
Running Multiple Async Operations in Parallel
Use Promise.all()
to execute multiple asynchronous operations concurrently.
async function fetchMultiple() {
try {
const [data1, data2] = await Promise.all([
fetch("https://jsonplaceholder.typicode.com/todos/1").then(res => res.json()),
fetch("https://jsonplaceholder.typicode.com/todos/2").then(res => res.json())
]);
console.log("Data 1:", data1);
console.log("Data 2:", data2);
} catch (error) {
console.error("Error fetching data:", error);
}
}
fetchMultiple();
Conclusion
- Callbacks were the first approach but led to nested, unreadable code.
- Promises introduced structured error handling and chaining.
- Async/Await provided a cleaner, synchronous-like way to handle asynchronous operations.
Understanding these techniques is essential for writing efficient JavaScript applications, especially for web development, API interactions, and modern frameworks.
References
MDN Web Docs: Promises
Learn about JavaScript promises and how to use them effectively.
MDN Web Docs: Async/Await
Learn about JavaScript promises and how to use them effectively.
JavaScript.info: Async JavaScript
Learn about JavaScript promises and how to use them effectively.