JavaScript Promises and Async Programming - Complete Guide
Introduction: JavaScript is single-threaded and synchronous by default. Asynchronous programming allows non-blocking operations like network requests, file operations, and timers. Promises and async/await are modern approaches to handle asynchronous code more effectively.
1. Understanding Asynchronous JavaScript
Traditional callback approach and its problems:
// Traditional callback approach
function fetchData(callback) {
setTimeout(() => {
const data = 'Some data';
callback(null, data);
}, 1000);
}
// Callback hell example
fetchData((err, data1) => {
if (err) return console.error(err);
processData1(data1, (err, data2) => {
if (err) return console.error(err);
processData2(data2, (err, data3) => {
if (err) return console.error(err);
console.log(data3);
});
});
});
2. Promise Basics
Creating and using promises:
// Creating a promise
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve('Operation successful!');
} else {
reject('Operation failed!');
}
}, 1000);
});
// Using the promise
myPromise
.then(result => {
console.log(result);
return result.toUpperCase();
})
.then(upperResult => {
console.log(upperResult);
})
.catch(error => {
console.error('Error:', error);
})
.finally(() => {
console.log('Promise completed');
});
3. Promise States
A Promise has three states:
Pending
Resolved (Fulfilled)
Rejected
- Pending: Initial state, neither fulfilled nor rejected
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
4. Promise Methods
Common Promise methods and their usage:
| Method |
Description |
Example |
| Promise.resolve() |
Creates a resolved promise |
Promise.resolve('Success') |
| Promise.reject() |
Creates a rejected promise |
Promise.reject('Error') |
| Promise.all() |
Waits for all promises to resolve |
Promise.all([p1, p2, p3]) |
| Promise.allSettled() |
Waits for all promises to settle |
Promise.allSettled([p1, p2, p3]) |
| Promise.race() |
Resolves/rejects with first settled promise |
Promise.race([p1, p2, p3]) |
| Promise.any() |
Resolves with first fulfilled promise |
Promise.any([p1, p2, p3]) |
5. Promise.all() Example
Running multiple promises in parallel:
// Simulating API calls
const fetchUser = () => new Promise(resolve => {
setTimeout(() => resolve({ id: 1, name: 'John' }), 1000);
});
const fetchPosts = () => new Promise(resolve => {
setTimeout(() => resolve(['Post 1', 'Post 2']), 800);
});
const fetchComments = () => new Promise(resolve => {
setTimeout(() => resolve(['Comment 1', 'Comment 2']), 1200);
});
// Using Promise.all
Promise.all([fetchUser(), fetchPosts(), fetchComments()])
.then(([user, posts, comments]) => {
console.log('User:', user);
console.log('Posts:', posts);
console.log('Comments:', comments);
})
.catch(error => {
console.error('Error:', error);
});
6. Async/Await Syntax
Modern syntax for working with promises:
// Async function returns a promise
async function fetchDataAsync() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// Using async/await
async function main() {
try {
const data = await fetchDataAsync();
console.log(data);
} catch (error) {
console.error('Failed to get data:', error);
}
}
main();
7. Error Handling in Async Functions
Proper error handling techniques:
// Error handling with try/catch
async function riskyOperation() {
try {
const result = await someAsyncFunction();
return result;
} catch (error) {
console.error('Operation failed:', error);
// Return a default value or rethrow
return null;
}
}
// Multiple async operations with error handling
async function processMultiple() {
try {
const [data1, data2, data3] = await Promise.all([
fetchData1(),
fetchData2(),
fetchData3()
]);
return { data1, data2, data3 };
} catch (error) {
console.error('One of the operations failed:', error);
throw new Error('Failed to process all data');
}
}
8. Practical Example: API Calls
Real-world example with error handling:
class ApiService {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async get(endpoint) {
try {
const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Failed to fetch ${endpoint}:`, error);
throw error;
}
}
async post(endpoint, data) {
try {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Failed to post to ${endpoint}:`, error);
throw error;
}
}
}
// Usage
const api = new ApiService('https://jsonplaceholder.typicode.com');
async function fetchUserData() {
try {
const user = await api.get('/users/1');
const posts = await api.get(`/users/1/posts`);
console.log('User:', user);
console.log('Posts:', posts);
} catch (error) {
console.error('Failed to fetch user data:', error);
}
}
9. Converting Callbacks to Promises
Modernizing legacy callback-based code:
// Converting callback to promise
function callbackToPromise(callbackFunction) {
return function(...args) {
return new Promise((resolve, reject) => {
callbackFunction(...args, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
};
}
}
// Using util.promisify in Node.js
const { promisify } = require('util');
const readFile = promisify(require('fs').readFile);
// Example with setTimeout
const delay = promisify(setTimeout);
async function example() {
console.log('Start');
await delay(1000); // Wait 1 second
console.log('End');
}
10. Performance Considerations
Optimizing async operations:
// Sequential execution (slower)
async function sequential() {
const result1 = await fetch('/api/data1');
const result2 = await fetch('/api/data2');
const result3 = await fetch('/api/data3');
return [result1, result2, result3];
}
// Parallel execution (faster)
async function parallel() {
const [result1, result2, result3] = await Promise.all([
fetch('/api/data1'),
fetch('/api/data2'),
fetch('/api/data3')
]);
return [result1, result2, result3];
}
// Conditional parallel execution
async function conditionalParallel() {
const promises = [];
if (needUser) promises.push(fetch('/api/user'));
if (needPosts) promises.push(fetch('/api/posts'));
if (needComments) promises.push(fetch('/api/comments'));
const results = await Promise.all(promises);
return results;
}
Performance Tip: Use Promise.all() for independent operations that can run in parallel, but be cautious with Promise.all() when you need to handle failures individually. Consider Promise.allSettled() when you need results from all promises regardless of success or failure.
11. Best Practices
- Always handle promise rejections with .catch() or try/catch
- Use async/await for cleaner, more readable code
- Prefer Promise.all() for parallel operations when appropriate
- Be careful with Promise.race() - it resolves with the first settled promise
- Use Promise.allSettled() when you need to wait for all promises regardless of success/failure
- Consider using AbortController for cancellable fetch requests
- Structure your async code to minimize nested callbacks
- Handle errors at the appropriate level in your application
- Use proper error types for better debugging
- Consider using libraries like axios for more advanced HTTP handling