JavaScript Promises
Taming Asynchronous Code
Explanation
What Are Promises?
A Promise is an object representing the eventual completion (or failure) of an asynchronous operation. Think of it like ordering food at a restaurant - you get a ticket (promise) that will eventually be fulfilled with your food (resolved) or you'll be told they're out (rejected).
const orderFood = new Promise((resolve, reject) => {
// Kitchen is working...
if (foodReady) {
resolve('🍔 Your burger is ready!');
} else {
reject('Sorry, we are out of burgers');
}
});
Promise States
- Pending: Initial state, neither fulfilled nor rejected
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
Why This Matters
Promises solve "callback hell" and make async code readable:
// Callback hell 😱
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
console.log(c);
});
});
});
// Promise chain 😊
getData()
.then(a => getMoreData(a))
.then(b => getEvenMoreData(b))
.then(c => console.log(c));
Demonstration
Example 1: Creating and Using Promises
// Creating a promise
const fetchUser = (userId) => {
return new Promise((resolve, reject) => {
// Simulate API call
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: 'Arthur', email: 'art@bpc.com' });
} else {
reject(new Error('Invalid user ID'));
}
}, 1000);
});
};
// Using the promise
fetchUser(1)
.then(user => {
console.log('User found:', user);
return user.name; // Pass to next .then
})
.then(name => {
console.log('Name is:', name);
})
.catch(error => {
console.error('Error:', error.message);
})
.finally(() => {
console.log('Request completed');
});
Output:
User found: { id: 1, name: 'Arthur', email: 'art@bpc.com' }
Name is: Arthur
Request completed
Example 2: Promise.all and Promise.race
const fetchUser = id => new Promise(resolve =>
setTimeout(() => resolve({ id, name: `User ${id}` }), 1000)
);
const fetchPosts = userId => new Promise(resolve =>
setTimeout(() => resolve([{ id: 1, title: 'Post 1' }]), 800)
);
const fetchComments = postId => new Promise(resolve =>
setTimeout(() => resolve(['Great post!', 'Thanks!']), 600)
);
// Promise.all - Wait for ALL to complete
Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
])
.then(([user, posts, comments]) => {
console.log('All data:', { user, posts, comments });
})
.catch(error => console.error('One failed:', error));
// Promise.race - First one wins
Promise.race([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
])
.then(firstResult => {
console.log('First to finish:', firstResult);
});
// Promise.allSettled - Get all results regardless of success/failure
Promise.allSettled([
Promise.resolve('Success'),
Promise.reject('Failed'),
Promise.resolve('Also success')
])
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Value:', result.value);
} else {
console.log('Reason:', result.reason);
}
});
});
Example 3: Real-World API Calls
// Fetch API returns promises
const getUsers = async () => {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
// Chaining API calls
const getUserWithPosts = (userId) => {
let userData;
return fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(user => {
userData = user;
return fetch(`/api/users/${userId}/posts`);
})
.then(res => res.json())
.then(posts => {
return { ...userData, posts };
});
};
// Error handling with specific catches
fetch('/api/data')
.then(res => {
if (res.status === 404) throw new Error('Not found');
if (res.status === 401) throw new Error('Unauthorized');
return res.json();
})
.then(data => console.log(data))
.catch(error => {
if (error.message === 'Unauthorized') {
// Redirect to login
} else {
// Show error message
}
});
Key Takeaways:
.then()handles success,.catch()handles errors.finally()runs regardless of outcomePromise.allfails fast - one rejection rejects allPromise.allSettledwaits for all, reports each status
Imitation
Challenge 1: Retry Logic
Task: Create a function that retries a failed promise up to N times.
retry(fetchData, 3); // Try fetchData up to 3 times
Solution
const retry = (fn, retries) => {
return fn().catch(error => {
if (retries > 0) {
console.log(`Retrying... ${retries} attempts left`);
return retry(fn, retries - 1);
}
throw error;
});
};
// Usage
retry(() => fetch('/flaky-api').then(r => r.json()), 3)
.then(data => console.log(data))
.catch(err => console.log('All retries failed'));
Challenge 2: Timeout Wrapper
Task: Create a function that adds a timeout to any promise.
withTimeout(fetchData(), 5000); // Reject if not resolved in 5 seconds
Solution
const withTimeout = (promise, ms) => {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms);
});
return Promise.race([promise, timeout]);
};
// Usage
withTimeout(fetch('/slow-api'), 5000)
.then(data => console.log(data))
.catch(err => console.log(err.message)); // 'Timeout' if too slow
Practice
Exercise 1: Sequential vs Parallel
Difficulty: Intermediate
You have 3 API calls that each take 1 second. Write two versions:
- Sequential (total ~3 seconds)
- Parallel (total ~1 second)
Exercise 2: Promise Queue
Difficulty: Advanced
Create a promise queue that runs promises with a concurrency limit:
const queue = new PromiseQueue(2); // Max 2 concurrent
queue.add(() => fetch('/api/1'));
queue.add(() => fetch('/api/2'));
queue.add(() => fetch('/api/3')); // Waits until one of the first two finishes
Summary
What you learned:
- Promise states: pending, fulfilled, rejected
- Creating promises with
new Promise() - Chaining with
.then(),.catch(),.finally() - Combining promises:
all,race,allSettled
Next Steps:
- Read: Async/Await
- Practice: Convert callback-based code to promises
- Build: Create a data fetching library with retry logic
