⏳ Asynchronous JavaScript
JavaScript is single-threaded, meaning it executes one operation at a time. Asynchronous programming allows you to perform long-running operations (like fetching data, reading files, or waiting for timers) without blocking the execution of other code.
Asynchronous code allows JavaScript to handle time-consuming tasks in the background while continuing to run other code. This is essential for building responsive web applications.
🔄 Synchronous vs Asynchronous Code
// Synchronous - executes line by line
console.log('First');
console.log('Second');
console.log('Third');
// Output (in order):
// First
// Second
// Third
// Blocking example
function slowOperation() {
// This blocks execution for 3 seconds
const start = Date.now();
while (Date.now() - start < 3000) {}
return 'Done';
}
console.log('Start');
const result = slowOperation(); // Blocks here!
console.log(result);
console.log('End');
// Asynchronous - doesn't wait
console.log('First');
setTimeout(() => {
console.log('Second (after 2 seconds)');
}, 2000);
console.log('Third');
// Output:
// First
// Third
// Second (after 2 seconds)
// Non-blocking example
console.log('Start');
setTimeout(() => {
console.log('This runs after 1 second');
}, 1000);
console.log('End (runs immediately)');
// Output:
// Start
// End (runs immediately)
// This runs after 1 second
📞 Callbacks
A callback is a function passed as an argument to another function, to be executed later when an asynchronous operation completes.
// Simple callback example
function greet(name, callback) {
console.log('Hello, ' + name);
callback();
}
greet('John', function() {
console.log('Callback executed!');
});
// Asynchronous callback
function fetchUserData(userId, callback) {
console.log('Fetching user data...');
setTimeout(() => {
const user = {
id: userId,
name: 'John Doe',
email: 'john@example.com'
};
callback(user);
}, 2000);
}
fetchUserData(1, function(user) {
console.log('User data received:', user);
});
// Callback with error handling
function fetchData(url, onSuccess, onError) {
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
onSuccess({ data: 'Some data' });
} else {
onError(new Error('Failed to fetch data'));
}
}, 1000);
}
fetchData(
'/api/users',
(data) => console.log('Success:', data),
(error) => console.error('Error:', error)
);
Callback Hell (Pyramid of Doom)
// ❌ Bad: Callback hell (hard to read and maintain)
fetchUser(userId, (user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
fetchReplies(comments[0].id, (replies) => {
console.log('Finally got replies:', replies);
}, (error) => {
console.error('Error fetching replies:', error);
});
}, (error) => {
console.error('Error fetching comments:', error);
});
}, (error) => {
console.error('Error fetching posts:', error);
});
}, (error) => {
console.error('Error fetching user:', error);
});
// This is hard to:
// - Read and understand
// - Handle errors properly
// - Maintain and debug
Deeply nested callbacks become difficult to read and maintain. This is why Promises and async/await were introduced.
🤝 Promises
A Promise is an object representing the eventual completion (or failure) of an asynchronous operation. It's a cleaner way to handle async code than callbacks.
Promise States
- Pending - Initial state, operation is ongoing
- Fulfilled - Operation completed successfully
- Rejected - Operation failed with an error
Creating Promises
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation here
const success = true;
if (success) {
resolve('Operation successful!'); // Fulfill the promise
} else {
reject('Operation failed!'); // Reject the promise
}
});
// Basic promise example
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data fetched successfully!');
}, 2000);
});
// Consuming the promise
promise.then((result) => {
console.log(result); // "Data fetched successfully!"
});
// Promise with error
const failingPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Something went wrong!'));
}, 1000);
});
failingPromise.catch((error) => {
console.error('Error:', error.message);
});
Using Promises (.then() and .catch())
// .then() - handles successful resolution
promise.then((result) => {
console.log('Success:', result);
});
// .catch() - handles rejection
promise.catch((error) => {
console.error('Error:', error);
});
// .finally() - runs regardless of outcome
promise.finally(() => {
console.log('Promise completed (success or failure)');
});
// Chaining all together
fetchData()
.then((data) => {
console.log('Data:', data);
return data;
})
.catch((error) => {
console.error('Error:', error);
})
.finally(() => {
console.log('Cleanup or loading indicator removal');
});
// Practical example
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({
id: userId,
name: 'John Doe',
email: 'john@example.com'
});
} else {
reject(new Error('Invalid user ID'));
}
}, 1000);
});
}
fetchUserData(1)
.then((user) => {
console.log('User:', user);
})
.catch((error) => {
console.error('Error:', error.message);
});
Promise Chaining
// ✅ Good: Promise chaining (much cleaner than callbacks)
fetchUser(userId)
.then((user) => {
console.log('User:', user);
return fetchPosts(user.id); // Return next promise
})
.then((posts) => {
console.log('Posts:', posts);
return fetchComments(posts[0].id);
})
.then((comments) => {
console.log('Comments:', comments);
return fetchReplies(comments[0].id);
})
.then((replies) => {
console.log('Replies:', replies);
})
.catch((error) => {
// Single error handler for entire chain
console.error('Error at any step:', error);
});
// Transforming data through chain
fetchUserData(1)
.then((user) => {
return user.name; // Extract name
})
.then((name) => {
return name.toUpperCase(); // Transform
})
.then((upperName) => {
console.log('Upper name:', upperName);
})
.catch((error) => {
console.error('Error:', error);
});
// Multiple operations
getUserId()
.then((id) => fetchUser(id))
.then((user) => fetchOrders(user.id))
.then((orders) => processOrders(orders))
.then((result) => console.log('Final result:', result))
.catch((error) => console.error('Error:', error));
⚡ Async/Await
async/await is modern syntax that makes asynchronous code look and behave more like synchronous code. It's built on top of Promises and makes code much more readable.
Async Functions
// async function always returns a Promise
async function myFunction() {
return 'Hello';
}
// Equivalent to:
function myFunction() {
return Promise.resolve('Hello');
}
// Using the async function
myFunction().then((result) => {
console.log(result); // "Hello"
});
// await keyword (only works inside async functions)
async function fetchData() {
const data = await someAsyncOperation();
console.log(data); // Wait for promise to resolve
}
// Basic example
async function getUserData() {
const response = await fetchUserData(1);
console.log('User:', response);
return response;
}
getUserData();
await - Waiting for Promises
// Without async/await (Promise chain)
function getUserInfo() {
fetchUser(1)
.then((user) => {
console.log('User:', user);
return fetchPosts(user.id);
})
.then((posts) => {
console.log('Posts:', posts);
})
.catch((error) => {
console.error('Error:', error);
});
}
// ✅ With async/await (much cleaner!)
async function getUserInfo() {
try {
const user = await fetchUser(1);
console.log('User:', user);
const posts = await fetchPosts(user.id);
console.log('Posts:', posts);
} catch (error) {
console.error('Error:', error);
}
}
// Multiple sequential operations
async function processUserData() {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
const replies = await fetchReplies(comments[0].id);
console.log('Got all data:', { user, posts, comments, replies });
}
// Returning values
async function getUsername(userId) {
const user = await fetchUser(userId);
return user.name; // Automatically wrapped in Promise
}
const name = await getUsername(1); // Can use await if in async context
console.log(name);
Error Handling with try/catch
// Error handling with try/catch
async function fetchData() {
try {
const data = await someAsyncOperation();
console.log('Data:', data);
return data;
} catch (error) {
console.error('Error occurred:', error.message);
throw error; // Re-throw if needed
}
}
// Multiple try/catch blocks
async function processData() {
let user;
let posts;
try {
user = await fetchUser(1);
} catch (error) {
console.error('Failed to fetch user:', error);
return; // Exit early
}
try {
posts = await fetchPosts(user.id);
} catch (error) {
console.error('Failed to fetch posts:', error);
posts = []; // Use default value
}
return { user, posts };
}
// finally block
async function fetchWithCleanup() {
try {
showLoadingSpinner();
const data = await fetchData();
return data;
} catch (error) {
console.error('Error:', error);
showErrorMessage(error.message);
} finally {
hideLoadingSpinner(); // Always runs
}
}
// Handling specific errors
async function smartFetch() {
try {
const data = await fetchData();
return data;
} catch (error) {
if (error.message.includes('404')) {
console.log('Resource not found');
} else if (error.message.includes('500')) {
console.log('Server error');
} else {
console.log('Unknown error:', error);
}
throw error;
}
}
🔀 Promise Methods
Promise.all() - Wait for All
// Promise.all() - waits for ALL promises to resolve
const promise1 = fetchUser(1);
const promise2 = fetchPosts(1);
const promise3 = fetchComments(1);
Promise.all([promise1, promise2, promise3])
.then(([user, posts, comments]) => {
console.log('All resolved:', { user, posts, comments });
})
.catch((error) => {
console.error('At least one failed:', error);
});
// With async/await
async function fetchAllData() {
try {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
console.log('User:', user);
console.log('Posts:', posts);
console.log('Comments:', comments);
} catch (error) {
console.error('One or more requests failed:', error);
}
}
// Practical example: Parallel API calls
async function getDashboardData() {
const [user, stats, notifications, messages] = await Promise.all([
fetchUser(),
fetchStats(),
fetchNotifications(),
fetchMessages()
]);
return { user, stats, notifications, messages };
}
// ⚠️ Important: If ANY promise rejects, Promise.all() rejects immediately
Promise.race() - First to Complete
// Promise.race() - resolves/rejects with first completed promise
const slow = new Promise((resolve) => {
setTimeout(() => resolve('Slow'), 3000);
});
const fast = new Promise((resolve) => {
setTimeout(() => resolve('Fast'), 1000);
});
Promise.race([slow, fast])
.then((result) => {
console.log(result); // "Fast" (first to complete)
});
// Practical example: Timeout pattern
function fetchWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Request timeout'));
}, timeout);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
// Usage
async function getData() {
try {
const response = await fetchWithTimeout('/api/data', 3000);
const data = await response.json();
console.log('Data:', data);
} catch (error) {
console.error('Error or timeout:', error.message);
}
}
Promise.allSettled() - Wait for All (No Rejection)
// Promise.allSettled() - waits for all, never rejects
const promises = [
Promise.resolve('Success 1'),
Promise.reject('Error 1'),
Promise.resolve('Success 2'),
Promise.reject('Error 2')
];
Promise.allSettled(promises)
.then((results) => {
results.forEach((result) => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.log('Failed:', result.reason);
}
});
});
// Output structure:
// [
// { status: 'fulfilled', value: 'Success 1' },
// { status: 'rejected', reason: 'Error 1' },
// { status: 'fulfilled', value: 'Success 2' },
// { status: 'rejected', reason: 'Error 2' }
// ]
// Practical example: Multiple independent API calls
async function fetchMultipleUsers(userIds) {
const promises = userIds.map((id) => fetchUser(id));
const results = await Promise.allSettled(promises);
const successful = results
.filter((r) => r.status === 'fulfilled')
.map((r) => r.value);
const failed = results
.filter((r) => r.status === 'rejected')
.map((r) => r.reason);
return { successful, failed };
}
Promise.any() - First to Succeed
// Promise.any() - resolves with first successful promise
const promise1 = Promise.reject('Error 1');
const promise2 = new Promise((resolve) => setTimeout(() => resolve('Success 2'), 1000));
const promise3 = new Promise((resolve) => setTimeout(() => resolve('Success 3'), 500));
Promise.any([promise1, promise2, promise3])
.then((result) => {
console.log(result); // "Success 3" (first to succeed)
})
.catch((error) => {
console.log('All promises failed:', error);
});
// Practical example: Fastest mirror
async function fetchFromFastestServer(urls) {
const promises = urls.map((url) => fetch(url));
try {
const response = await Promise.any(promises);
return await response.json();
} catch (error) {
throw new Error('All servers failed');
}
}
// Usage
const mirrors = [
'https://api1.example.com/data',
'https://api2.example.com/data',
'https://api3.example.com/data'
];
const data = await fetchFromFastestServer(mirrors);
🎯 Practical Examples
Example 1: Sequential vs Parallel Execution
// ❌ Sequential (slow - 3 seconds total)
async function fetchSequential() {
const start = Date.now();
const user = await fetchUser(1); // 1 second
const posts = await fetchPosts(1); // 1 second
const comments = await fetchComments(1); // 1 second
console.log(`Sequential took: ${Date.now() - start}ms`);
// Output: Sequential took: 3000ms
}
// ✅ Parallel (fast - 1 second total)
async function fetchParallel() {
const start = Date.now();
// All start at the same time
const [user, posts, comments] = await Promise.all([
fetchUser(1), // 1 second
fetchPosts(1), // 1 second
fetchComments(1) // 1 second
]);
console.log(`Parallel took: ${Date.now() - start}ms`);
// Output: Parallel took: 1000ms (much faster!)
}
// When to use each:
// - Sequential: When next operation depends on previous result
// - Parallel: When operations are independent
Example 2: Retry Logic
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
console.log(`Attempt ${i + 1} of ${maxRetries}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.log(`Attempt ${i + 1} failed:`, error.message);
// If last attempt, throw error
if (i === maxRetries - 1) {
throw new Error(`Failed after ${maxRetries} attempts`);
}
// Wait before retry (exponential backoff)
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
}
// Usage
try {
const data = await fetchWithRetry('/api/data', 3);
console.log('Data:', data);
} catch (error) {
console.error('All retries failed:', error.message);
}
Example 3: Data Pipeline
// Complete async data pipeline
async function processUserData(userId) {
try {
// 1. Fetch user
console.log('Fetching user...');
const user = await fetchUser(userId);
// 2. Fetch related data in parallel
console.log('Fetching related data...');
const [posts, friends, settings] = await Promise.all([
fetchPosts(user.id),
fetchFriends(user.id),
fetchSettings(user.id)
]);
// 3. Process data
console.log('Processing data...');
const processedPosts = posts.map((post) => ({
...post,
wordCount: post.content.split(' ').length
}));
// 4. Calculate statistics
const stats = {
totalPosts: posts.length,
totalFriends: friends.length,
avgPostLength: processedPosts.reduce((sum, p) => sum + p.wordCount, 0) / posts.length
};
// 5. Return complete profile
return {
user,
posts: processedPosts,
friends,
settings,
stats
};
} catch (error) {
console.error('Pipeline error:', error);
throw error;
}
}
// Usage
processUserData(1)
.then((profile) => {
console.log('Complete profile:', profile);
displayUserProfile(profile);
})
.catch((error) => {
showErrorMessage(error.message);
});
Example 4: Loading States
// Managing UI loading states with async operations
async function loadUserDashboard(userId) {
const loadingElement = document.getElementById('loading');
const contentElement = document.getElementById('content');
const errorElement = document.getElementById('error');
try {
// Show loading
loadingElement.style.display = 'block';
contentElement.style.display = 'none';
errorElement.style.display = 'none';
// Fetch data
const [user, stats, notifications] = await Promise.all([
fetchUser(userId),
fetchUserStats(userId),
fetchNotifications(userId)
]);
// Hide loading, show content
loadingElement.style.display = 'none';
contentElement.style.display = 'block';
// Render dashboard
renderDashboard({ user, stats, notifications });
} catch (error) {
// Hide loading, show error
loadingElement.style.display = 'none';
errorElement.style.display = 'block';
errorElement.textContent = `Error: ${error.message}`;
console.error('Dashboard load error:', error);
}
}
// Alternative pattern with state object
async function loadData() {
const state = {
loading: true,
data: null,
error: null
};
updateUI(state);
try {
state.data = await fetchData();
state.loading = false;
} catch (error) {
state.error = error;
state.loading = false;
}
updateUI(state);
}
✅ Best Practices
- Use
async/awaitinstead of promise chains for better readability - Always use
try/catchblocks with async/await - Use
Promise.all()for parallel operations to improve performance - Handle errors at the appropriate level (don't swallow errors)
- Use
Promise.allSettled()when you need all results regardless of failures - Implement timeout patterns for network requests
- Consider retry logic for failed requests
- Use
finallyfor cleanup operations (hiding loaders, etc.) - Avoid mixing callbacks and promises (pick one pattern)
- Remember:
awaitonly works insideasyncfunctions
⚠️ Common Mistakes
// ❌ Mistake 1: Forgetting await
async function fetchData() {
const data = fetch('/api/data'); // Missing await!
console.log(data); // This is a Promise, not the data!
}
// ✅ Correct
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data); // Now this is the actual data
}
// ❌ Mistake 2: Not handling errors
async function getData() {
const data = await fetch('/api/data'); // No try/catch!
return data;
}
// ✅ Correct
async function getData() {
try {
const response = await fetch('/api/data');
return await response.json();
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// ❌ Mistake 3: Sequential when could be parallel
async function loadPage() {
const user = await fetchUser(); // Waits
const posts = await fetchPosts(); // Then waits
const comments = await fetchComments(); // Then waits
}
// ✅ Correct (if operations are independent)
async function loadPage() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
}
// ❌ Mistake 4: Using await in loops inefficiently
async function processItems(items) {
for (const item of items) {
await processItem(item); // Processes one at a time
}
}
// ✅ Correct (if order doesn't matter)
async function processItems(items) {
await Promise.all(items.map((item) => processItem(item)));
}
// ❌ Mistake 5: Forgetting to return in promise chains
fetchData()
.then((data) => {
processData(data); // Missing return!
})
.then((result) => {
console.log(result); // undefined!
});
// ✅ Correct
fetchData()
.then((data) => {
return processData(data); // Return the promise
})
.then((result) => {
console.log(result); // Now has the result
});
- Asynchronous code allows non-blocking operations
- Callbacks are the original async pattern (can lead to callback hell)
- Promises provide cleaner async code with .then() and .catch()
- async/await makes async code look synchronous and is easier to read
- try/catch handles errors in async/await
- Promise.all() runs promises in parallel for better performance
- Promise.race() returns the first completed promise
- Promise.allSettled() waits for all promises without rejecting
- Error handling is crucial in async operations