The Evolution of Asynchronous JavaScript
Imagine you're cooking a complex meal. With traditional Promises, you'd write out separate instructions for each step and chain them together. With async/await, you can write the recipe as if you're doing each step one after another. Let's see this evolution in code:
Traditional Promise Approach
// Cooking dinner with traditional Promises
function prepareIngredients() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Ingredients ready!');
}, 1000);
});
}
function cook() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Food cooked!');
}, 2000);
});
}
function serve() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Dinner is served!');
}, 500);
});
}
// Promise chain approach
function makeDinner() {
console.log('Starting dinner preparation...');
prepareIngredients()
.then(result => {
console.log(result);
return cook();
})
.then(result => {
console.log(result);
return serve();
})
.then(result => {
console.log(result);
})
.catch(error => {
console.error('Dinner preparation failed:', error);
});
}
Modern async/await Approach
// The same dinner preparation with async/await
async function makeDinnerModern() {
try {
console.log('Starting dinner preparation...');
const ingredients = await prepareIngredients();
console.log(ingredients);
const cookedFood = await cook();
console.log(cookedFood);
const served = await serve();
console.log(served);
return 'Dinner preparation complete!';
} catch (error) {
console.error('Dinner preparation failed:', error);
}
}
Understanding async Functions
When you mark a function with async, JavaScript automatically wraps its return value in a Promise. Think of it like putting your function's result in a special delivery package:
// Regular function
function normalGreeting() {
return "Hello!";
}
console.log(normalGreeting()); // "Hello!"
// async function
async function asyncGreeting() {
return "Hello!";
}
console.log(asyncGreeting()); // Promise { 'Hello!' }
// Using the async greeting
asyncGreeting().then(message => console.log(message)); // "Hello!"
// Real-world example: Fetching user data
async function getUserProfile(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('User not found');
}
const userData = await response.json();
return {
...userData,
lastFetched: new Date()
};
} catch (error) {
console.error('Failed to fetch user:', error);
return null;
}
}
The Power of await
The await keyword is like a "pause and wait for result" button. It can only be used inside async functions and makes asynchronous code look synchronous:
// Simulating a database query
function queryDatabase(query) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
rows: [
{ id: 1, name: 'Product A' },
{ id: 2, name: 'Product B' }
]
});
}, 1000);
});
}
// Traditional Promise approach
function getProducts() {
queryDatabase('SELECT * FROM products')
.then(result => {
console.log(result.rows);
})
.catch(error => {
console.error('Query failed:', error);
});
}
// Modern async/await approach
async function getProductsModern() {
try {
const result = await queryDatabase('SELECT * FROM products');
console.log(result.rows);
return result.rows;
} catch (error) {
console.error('Query failed:', error);
return [];
}
}
Practical Patterns with async/await
1. Sequential Operations
async function processOrder(orderId) {
try {
// Each step waits for the previous to complete
const order = await fetchOrder(orderId);
const validatedOrder = await validateOrder(order);
const processedOrder = await processPayment(validatedOrder);
const shippingLabel = await createShippingLabel(processedOrder);
return {
orderId,
status: 'completed',
shippingLabel
};
} catch (error) {
await handleOrderError(orderId, error);
throw error;
}
}
2. Parallel Operations
async function loadDashboard(userId) {
try {
// Start all requests simultaneously
const [
userProfile,
userPreferences,
recentOrders
] = await Promise.all([
fetchUserProfile(userId),
fetchUserPreferences(userId),
fetchRecentOrders(userId)
]);
return {
profile: userProfile,
preferences: userPreferences,
orders: recentOrders
};
} catch (error) {
console.error('Dashboard loading failed:', error);
throw error;
}
}
3. Error Handling Patterns
async function retryOperation(operation, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxAttempts) throw error;
console.log(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
// Usage example
async function fetchDataWithRetry() {
try {
const data = await retryOperation(async () => {
const response = await fetch('https://api.example.com/data');
if (!response.ok) throw new Error('Bad response');
return response.json();
});
return data;
} catch (error) {
console.error('All retry attempts failed:', error);
return null;
}
}
Best Practices and Common Patterns
1. Clean Error Handling
// Helper function for consistent error handling
async function safeAsync(promise) {
try {
const result = await promise;
return [null, result];
} catch (error) {
return [error, null];
}
}
// Usage
async function getUserData(userId) {
const [error, data] = await safeAsync(fetchUserData(userId));
if (error) {
console.error('Failed to fetch user data:', error);
return null;
}
return data;
}
2. Handling Multiple Promises Efficiently
async function processItems(items) {
// Process items in batches of 5
const batchSize = 5;
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchPromises = batch.map(processItem);
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
}
return results;
}
// With progress tracking
async function processItemsWithProgress(items, onProgress) {
const results = [];
const total = items.length;
for (let i = 0; i < items.length; i++) {
const result = await processItem(items[i]);
results.push(result);
onProgress({
completed: i + 1,
total,
percentComplete: ((i + 1) / total) * 100
});
}
return results;
}
Advanced Patterns
1. Cancellable Operations
function createCancellableOperation() {
let isCancelled = false;
const operation = async () => {
// Simulate long operation
for (let i = 0; i < 10; i++) {
if (isCancelled) {
throw new Error('Operation cancelled');
}
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`Step ${i + 1} completed`);
}
};
const cancel = () => {
isCancelled = true;
};
return { operation, cancel };
}
// Usage
async function main() {
const { operation, cancel } = createCancellableOperation();
// Cancel after 3 seconds
setTimeout(cancel, 3000);
try {
await operation();
console.log('Operation completed');
} catch (error) {
if (error.message === 'Operation cancelled') {
console.log('Operation was cancelled');
} else {
console.error('Operation failed:', error);
}
}
}