JavaScript is inherently single-threaded, but it needs to handle time-consuming operation like network requests, file reading, and database queries without blocking the main thread. This challenge led to the fascinating evolution of asynchronous programming technique.
The Problem with Traditional Callbacks
function fetchUserData(userId, callback) {
$.ajax({
url: '/api/users/' + userId,
success: function(userData) {
fetchUserPosts(userData.id, function(posts) {
fetchPostComments(posts[0].id, function(comments) {
renderPage(userData, posts, comments);
}, function(error) {
console.error('Failed to fetch comments');
});
}, function(error) {
console.error('Failed to fetch posts');
});
},
error: function(error) {
console.error('Failed to fetch user data');
}
});
}
This code demonstrates the infamous “calback hell”
- Deeply nested function calls
- Complex error handing
- Reduced readability
- Difficult to manage sequential or parallel operations
Classic Countdown Example: Callback Chaos
console.log("3…");
setTimeout(() => {
console.log("2…");
setTimeout(() => {
console.log("1…");
setTimeout(() => {
console.log("Happy New Year!!");
}, 1000);
}, 1000);
}, 1000);
Promises
Promises introduced a more structured approach to handing asynchronous operations:
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
$.ajax({
url: '/api/users/' + userId,
success: (userData) => resolve(userData),
error: (error) => reject(error)
});
});
}
// Promise chaining
fetchUserData(123)
.then(userData => fetchUserPosts(userData.id))
.then(posts => {
console.log('User posts:', posts);
})
.catch(error => {
console.error('An error occurred:', error);
});
Promises Methods
1. Promises.all()
async function fetchMultipleResources() {
try {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
} catch (error) {
console.error('One request failed');
}
}
2. Promises.race()
const fastest = await Promise.race([
fetch('https://api1.com/data'),
fetch('https://api2.com/data'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000)
)
]);
Async/Await
async function getUserContent(userId) {
try {
const userData = await fetchUserData(userId);
const posts = await fetchUserPosts(userData.id);
const comments = await fetchPostComments(posts[0].id);
return { userData, posts, comments };
} catch (error) {
console.error('Failed to fetch user content:', error);
}
}
Refactored Countdown Example
function createDelay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function newYearCountdown() {
console.log("3…");
await createDelay(1000);
console.log("2…");
await createDelay(1000);
console.log("1…");
await createDelay(1000);
console.log("Happy New Year!!");
}
Parallel vs Sequential Execution
// Parallel Execution
async function fetchParallel() {
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
]);
}
// Sequential Execution
async function fetchSequential() {
const users = await fetchUsers();
const posts = await fetchPosts(users[0].id);
}
Best Practices
- Always Handle Errors
- Use
.catch()or try/catch blocks - Provide meaningful error messages
- Use
2. Avoid Unnecessary Promise Wrapping
// Bad
function bad() {
return new Promise(resolve => resolve(42));
}
// Good
function good() {
return Promise.resolve(42);
}
Use Async/Await for Most Use Cases
- Looks like synchronous code
- More readable
- Easier error handling