Introduction to Promises
Remember the callback hell we discussed? Promises are JavaScript's elegant solution to this problem. Think of a Promise as a restaurant receipt - when you order food, you get a receipt (promise) that represents your future meal. The meal might arrive successfully, or there might be a problem in the kitchen.
What is a Promise?
A Promise is an object representing the eventual completion or failure of an asynchronous operation. It can be in one of three states:
- Pending: Initial state, operation in progress
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
Creating a Promise
Let's start with the basics of creating and using promises:
// Creating a simple promise
const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation here
setTimeout(() => {
const success = true; // Simulate success/failure
if (success) {
resolve("Operation successful!");
} else {
reject("Operation failed!");
}
}, 1000);
});
// Using the promise
myPromise
.then(result => {
console.log(result); // "Operation successful!"
})
.catch(error => {
console.error(error);
});
Promise Anatomy
Let's break down the components of a Promise:
// 1. The Promise constructor
const promise = new Promise((resolve, reject) => {
// 2. Executor function with resolve and reject parameters
// 3. Your async operation
setTimeout(() => {
const randomNumber = Math.random();
if (randomNumber > 0.5) {
// 4. Call resolve for success
resolve(randomNumber);
} else {
// 5. Call reject for failure
reject(new Error('Number too small'));
}
}, 1000);
});
// 6. Handling the promise
promise
.then(value => {
// 7. Success handler
console.log('Success:', value);
})
.catch(error => {
// 8. Error handler
console.error('Error:', error.message);
})
.finally(() => {
// 9. Cleanup (always runs)
console.log('Operation complete');
});
Converting Callbacks to Promises
Let's transform our callback-based functions into promise-based ones:
Before (Callback)
function fetchUserDataCallback(userId, callback) {
setTimeout(() => {
if (!userId) {
callback(new Error('Invalid user ID'), null);
return;
}
callback(null, { id: userId, name: 'John Doe' });
}, 1000);
}
After (Promise)
function fetchUserDataPromise(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!userId) {
reject(new Error('Invalid user ID'));
return;
}
resolve({ id: userId, name: 'John Doe' });
}, 1000);
});
}
// Usage is much cleaner
fetchUserDataPromise(123)
.then(user => console.log('User:', user))
.catch(error => console.error('Error:', error));
Promise Chaining
One of the most powerful features of Promises is chaining. Each .then() returns a new Promise, allowing us to chain operations:
// Sequential operations with promise chaining
function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: 'Alice' });
}, 1000);
});
}
function fetchPosts(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, userId, title: 'Post 1' },
{ id: 2, userId, title: 'Post 2' }
]);
}, 1000);
});
}
function fetchComments(postId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, postId, text: 'Great post!' },
{ id: 2, postId, text: 'Thanks for sharing!' }
]);
}, 1000);
});
}
// Clean promise chain
fetchUser(1)
.then(user => {
console.log('User:', user);
return fetchPosts(user.id);
})
.then(posts => {
console.log('Posts:', posts);
return fetchComments(posts[0].id);
})
.then(comments => {
console.log('Comments:', comments);
})
.catch(error => {
console.error('Error in chain:', error);
});
Compare this to the callback hell version - much more readable!
Promise Static Methods
JavaScript provides several useful static methods on the Promise object:
Promise.resolve() and Promise.reject()
// Create immediately resolved/rejected promises
const resolvedPromise = Promise.resolve('Instant success!');
const rejectedPromise = Promise.reject(new Error('Instant failure!'));
resolvedPromise.then(value => console.log(value));
rejectedPromise.catch(error => console.error(error));
Promise.all()
// Wait for all promises to complete
const promise1 = fetchUser(1);
const promise2 = fetchPosts(1);
const promise3 = fetchComments(1);
Promise.all([promise1, promise2, promise3])
.then(([user, posts, comments]) => {
console.log('All data loaded:', { user, posts, comments });
})
.catch(error => {
console.error('One or more promises failed:', error);
});
Promise.race()
// Resolves/rejects with the first settled promise
const slowPromise = new Promise(resolve =>
setTimeout(() => resolve('Slow'), 3000)
);
const fastPromise = new Promise(resolve =>
setTimeout(() => resolve('Fast'), 1000)
);
Promise.race([slowPromise, fastPromise])
.then(winner => console.log('Winner:', winner)); // 'Fast'
Promise.allSettled()
// Wait for all promises to settle (resolve or reject)
const promises = [
Promise.resolve('Success'),
Promise.reject(new Error('Failure')),
Promise.resolve('Another success')
];
Promise.allSettled(promises)
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.log('Failed:', result.reason);
}
});
});
Real-World Examples
Example 1: API Request with Retry Logic
function fetchWithRetry(url, maxRetries = 3) {
return new Promise((resolve, reject) => {
function attempt(retriesLeft) {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => resolve(data))
.catch(error => {
if (retriesLeft === 0) {
reject(error);
} else {
console.log(`Retrying... ${retriesLeft} attempts left`);
setTimeout(() => attempt(retriesLeft - 1), 1000);
}
});
}
attempt(maxRetries);
});
}
// Usage
fetchWithRetry('https://api.example.com/data')
.then(data => console.log('Success:', data))
.catch(error => console.error('Failed after retries:', error));
Example 2: Loading Multiple Resources
function loadUserDashboard(userId) {
// Load multiple resources in parallel
const userPromise = fetch(`/api/users/${userId}`).then(r => r.json());
const settingsPromise = fetch(`/api/settings/${userId}`).then(r => r.json());
const notificationsPromise = fetch(`/api/notifications/${userId}`).then(r => r.json());
return Promise.all([userPromise, settingsPromise, notificationsPromise])
.then(([user, settings, notifications]) => {
return {
user,
settings,
notifications,
loadedAt: new Date()
};
});
}
// Usage
loadUserDashboard(123)
.then(dashboard => {
console.log('Dashboard loaded:', dashboard);
// Update UI with dashboard data
})
.catch(error => {
console.error('Failed to load dashboard:', error);
// Show error message to user
});
Common Promise Patterns
Pattern 1: Promisifying Callbacks
// Convert any callback-based function to promise-based
function promisify(callbackBasedFunction) {
return function(...args) {
return new Promise((resolve, reject) => {
callbackBasedFunction(...args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
}
// Example usage
const readFileAsync = promisify(fs.readFile);
readFileAsync('file.txt', 'utf8')
.then(content => console.log(content))
.catch(error => console.error(error));
Pattern 2: Sequential Execution
// Execute promises in sequence
async function executeSequentially(tasks) {
const results = [];
for (const task of tasks) {
try {
const result = await task();
results.push(result);
} catch (error) {
console.error('Task failed:', error);
results.push(null);
}
}
return results;
}
// Usage
const tasks = [
() => fetchUser(1),
() => fetchPosts(1),
() => fetchComments(1)
];
executeSequentially(tasks)
.then(results => console.log('All tasks complete:', results));
Error Handling Best Practices
// Always catch errors at the end of promise chains
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => {
// Process comments
})
.catch(error => {
// Handle any error in the chain
console.error('Operation failed:', error);
// You can also check error types
if (error instanceof NetworkError) {
showOfflineMessage();
} else if (error instanceof ValidationError) {
showValidationErrors(error.errors);
} else {
showGenericError();
}
});
// Creating custom error types
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = 'NetworkError';
}
}
class ValidationError extends Error {
constructor(errors) {
super('Validation failed');
this.name = 'ValidationError';
this.errors = errors;
}
}
Practice Exercises
Exercise 1: Promise-based Timer
Create a promise-based delay function:
function delay(ms) {
// Your code here
// Should return a promise that resolves after ms milliseconds
}
// Test your function
delay(2000)
.then(() => console.log('2 seconds have passed'));
Exercise 2: Promise Chain Calculator
Create a calculator using promise chaining:
function calculate(initialValue) {
// Return an object with methods that return promises
// Each method should modify the value and return the calculator
}
// Usage example
calculate(5)
.add(3)
.multiply(2)
.subtract(4)
.divide(2)
.getResult()
.then(result => console.log(result)); // Should output 6
Key Takeaways
- Promises provide a cleaner way to handle asynchronous operations
- They have three states: pending, fulfilled, and rejected
- Promise chaining eliminates callback hell
- Static methods like Promise.all() enable powerful patterns
- Always handle errors with .catch() at the end of chains
Looking Ahead
Next, we'll explore async/await syntax, which makes working with Promises even more elegant by allowing us to write asynchronous code that looks synchronous.
// Sneak peek at async/await
async function loadDashboard() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
} catch (error) {
console.error('Error:', error);
}
}