Module 17: Promises
Promises represent the eventual completion (or failure) of an asynchronous operation. They provide a cleaner, more elegant way to handle async code than callbacks.
1. Understanding Promises
1.1 What is a Promise?
A Promise is an object representing the eventual completion or failure of an async operation.
Promise States:
- Pending – Initial state, neither fulfilled nor rejected
- Fulfilled – Operation completed successfully
- Rejected – Operation failed
const promise = new Promise((resolve, reject) => {
// Async operation
setTimeout(() => {
const success = true;
if (success) {
resolve('Operation successful!'); // Fulfill
} else {
reject('Operation failed!'); // Reject
}
}, 1000);
});
console.log(promise); // Promise { <pending> }
1.2 Why Promises?
// ❌ Callback Hell
getUserData(userId, function(error, user) {
if (error) return console.error(error);
getOrders(user.id, function(error, orders) {
if (error) return console.error(error);
getOrderDetails(orders[0].id, function(error, details) {
if (error) return console.error(error);
console.log(details);
});
});
});
// ✅ Promise Chain
getUserData(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => console.log(details))
.catch(error => console.error(error));
2. Creating Promises
2.1 Basic Promise
const myPromise = new Promise((resolve, reject) => {
// resolve(value) - Call when successful
// reject(reason) - Call when failed
const success = true;
if (success) {
resolve('Success!');
} else {
reject('Failed!');
}
});
2.2 Async Operation Example
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const users = {
1: { id: 1, name: 'John' },
2: { id: 2, name: 'Jane' }
};
const user = users[id];
if (user) {
resolve(user);
} else {
reject(new Error('User not found'));
}
}, 1000);
});
}
// Usage
fetchUser(1)
.then(user => console.log('User:', user))
.catch(error => console.error('Error:', error.message));
2.3 Promise.resolve() / Promise.reject()
// Create fulfilled promise
const fulfilled = Promise.resolve('Success');
fulfilled.then(value => console.log(value)); // "Success"
// Create rejected promise
const rejected = Promise.reject('Error');
rejected.catch(error => console.error(error)); // "Error"
// Wrapping values
Promise.resolve(42).then(value => console.log(value)); // 42
Promise.resolve([1, 2, 3]).then(arr => console.log(arr)); // [1, 2, 3]
3. Consuming Promises
3.1 then()
const promise = fetchUser(1);
// Single then
promise.then(user => {
console.log('User:', user);
});
// Chaining then
promise
.then(user => {
console.log('User:', user);
return user.name;
})
.then(name => {
console.log('Name:', name);
return name.toUpperCase();
})
.then(upperName => {
console.log('Upper name:', upperName);
});
3.2 catch()
fetchUser(999)
.then(user => console.log(user))
.catch(error => {
console.error('Error occurred:', error.message);
});
// Catch in chain
fetchUser(1)
.then(user => {
if (!user.email) {
throw new Error('User has no email');
}
return user.email;
})
.then(email => console.log('Email:', email))
.catch(error => console.error('Error:', error.message));
3.3 finally()
let isLoading = true;
fetchUser(1)
.then(user => {
console.log('User:', user);
})
.catch(error => {
console.error('Error:', error);
})
.finally(() => {
isLoading = false;
console.log('Request completed');
});
4. Promise Chaining
4.1 Sequential Operations
fetchUser(1)
.then(user => {
console.log('User:', user);
return getOrders(user.id); // Return new promise
})
.then(orders => {
console.log('Orders:', orders);
return getOrderDetails(orders[0].id);
})
.then(details => {
console.log('Details:', details);
})
.catch(error => {
console.error('Error in chain:', error);
});
4.2 Returning Values
Promise.resolve(5)
.then(num => {
console.log(num); // 5
return num * 2;
})
.then(num => {
console.log(num); // 10
return num + 3;
})
.then(num => {
console.log(num); // 13
});
4.3 Returning Promises
function step1() {
return new Promise(resolve => {
setTimeout(() => resolve('Step 1 done'), 1000);
});
}
function step2(prevResult) {
return new Promise(resolve => {
setTimeout(() => resolve(prevResult + ' → Step 2 done'), 1000);
});
}
function step3(prevResult) {
return new Promise(resolve => {
setTimeout(() => resolve(prevResult + ' → Step 3 done'), 1000);
});
}
step1()
.then(result => {
console.log(result);
return step2(result);
})
.then(result => {
console.log(result);
return step3(result);
})
.then(result => {
console.log(result);
});
5. Error Handling
5.1 Propagation
fetchUser(1)
.then(user => {
console.log('User:', user);
throw new Error('Something went wrong');
})
.then(result => {
// This won't execute
console.log('This will be skipped');
})
.catch(error => {
// Error is caught here
console.error('Caught:', error.message);
});
5.2 Recovery
fetchUser(999)
.then(user => console.log(user))
.catch(error => {
console.error('Error:', error.message);
// Return fallback value
return { id: 0, name: 'Guest' };
})
.then(user => {
// Continues with fallback user
console.log('User:', user);
});
5.3 Multiple Catch Blocks
fetchUser(1)
.then(user => {
if (!user.active) {
throw new Error('User inactive');
}
return getOrders(user.id);
})
.catch(error => {
console.error('User error:', error);
throw error; // Re-throw
})
.then(orders => {
return processOrders(orders);
})
.catch(error => {
console.error('Order error:', error);
});
6. Promise Static Methods
6.1 Promise.all()
// Wait for all promises to resolve
const promise1 = fetchUser(1);
const promise2 = fetchUser(2);
const promise3 = fetchUser(3);
Promise.all([promise1, promise2, promise3])
.then(users => {
console.log('All users:', users);
// [user1, user2, user3]
})
.catch(error => {
// If any promise rejects
console.error('Error:', error);
});
// Practical example
Promise.all([
fetch('https://api.example.com/users'),
fetch('https://api.example.com/posts'),
fetch('https://api.example.com/comments')
])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([users, posts, comments]) => {
console.log('Users:', users);
console.log('Posts:', posts);
console.log('Comments:', comments);
})
.catch(error => console.error('Error:', error));
6.2 Promise.allSettled()
// Wait for all promises to settle (fulfill or reject)
const promises = [
fetchUser(1),
fetchUser(999), // Will fail
fetchUser(2)
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} succeeded:`, result.value);
} else {
console.log(`Promise ${index} failed:`, result.reason);
}
});
});
// Output:
// Promise 0 succeeded: { id: 1, name: 'John' }
// Promise 1 failed: Error: User not found
// Promise 2 succeeded: { id: 2, name: 'Jane' }
6.3 Promise.race()
// Returns first promise to settle
const fast = new Promise(resolve => setTimeout(() => resolve('Fast'), 100));
const slow = new Promise(resolve => setTimeout(() => resolve('Slow'), 1000));
Promise.race([fast, slow])
.then(result => console.log(result)) // "Fast"
.catch(error => console.error(error));
// Timeout pattern
function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}
fetchWithTimeout('https://api.example.com/data', 3000)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error.message));
6.4 Promise.any()
// Returns first fulfilled promise (ignores rejections)
const p1 = Promise.reject('Error 1');
const p2 = new Promise(resolve => setTimeout(() => resolve('Success'), 100));
const p3 = Promise.reject('Error 3');
Promise.any([p1, p2, p3])
.then(result => console.log(result)) // "Success"
.catch(error => console.error(error));
// If all reject
Promise.any([
Promise.reject('Error 1'),
Promise.reject('Error 2'),
Promise.reject('Error 3')
])
.then(result => console.log(result))
.catch(error => {
console.error('All failed:', error); // AggregateError
});
7. Promise Patterns
7.1 Sequential Execution
const tasks = [
() => fetchUser(1),
() => fetchUser(2),
() => fetchUser(3)
];
// Execute sequentially
function sequential(tasks) {
return tasks.reduce((promise, task) => {
return promise.then(results => {
return task().then(result => [...results, result]);
});
}, Promise.resolve([]));
}
sequential(tasks)
.then(results => console.log('All results:', results))
.catch(error => console.error('Error:', error));
7.2 Retry Logic
function retry(fn, maxAttempts, delay = 1000) {
return new Promise((resolve, reject) => {
let attempts = 0;
function attempt() {
attempts++;
fn()
.then(resolve)
.catch(error => {
if (attempts < maxAttempts) {
console.log(`Attempt ${attempts} failed, retrying...`);
setTimeout(attempt, delay);
} else {
reject(error);
}
});
}
attempt();
});
}
// Usage
retry(() => fetchUser(999), 3, 1000)
.then(user => console.log('Success:', user))
.catch(error => console.error('All attempts failed:', error.message));
7.3 Debounced Promise
function debouncePromise(fn, delay) {
let timeoutId;
let latestResolve;
let latestReject;
return function(...args) {
clearTimeout(timeoutId);
return new Promise((resolve, reject) => {
latestResolve = resolve;
latestReject = reject;
timeoutId = setTimeout(() => {
fn(...args)
.then(latestResolve)
.catch(latestReject);
}, delay);
});
};
}
// Usage
const debouncedFetch = debouncePromise(fetchUser, 300);
// Only last call executes
debouncedFetch(1);
debouncedFetch(2);
debouncedFetch(3).then(user => console.log('User:', user));
7.4 Promise Queue
class PromiseQueue {
constructor(concurrency = 1) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
add(promiseGenerator) {
return new Promise((resolve, reject) => {
this.queue.push({ promiseGenerator, resolve, reject });
this.process();
});
}
process() {
if (this.running >= this.concurrency || this.queue.length === 0) {
return;
}
this.running++;
const { promiseGenerator, resolve, reject } = this.queue.shift();
promiseGenerator()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--;
this.process();
});
}
}
// Usage
const queue = new PromiseQueue(2); // Max 2 concurrent
queue.add(() => fetchUser(1)).then(console.log);
queue.add(() => fetchUser(2)).then(console.log);
queue.add(() => fetchUser(3)).then(console.log);
8. Practical Examples
8.1 API Client
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
}
// Usage
const api = new APIClient('https://api.example.com');
api.get('/users')
.then(users => console.log('Users:', users))
.catch(error => console.error('Error:', error));
8.2 Image Loader
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
img.src = url;
});
}
// Load single image
loadImage('/image.jpg')
.then(img => {
document.body.appendChild(img);
})
.catch(error => console.error(error));
// Load multiple images
const imageUrls = ['/img1.jpg', '/img2.jpg', '/img3.jpg'];
Promise.all(imageUrls.map(url => loadImage(url)))
.then(images => {
images.forEach(img => document.body.appendChild(img));
})
.catch(error => console.error('Failed to load images:', error));
8.3 Cache with Promises
class PromiseCache {
constructor() {
this.cache = new Map();
}
get(key, promiseGenerator) {
if (this.cache.has(key)) {
return Promise.resolve(this.cache.get(key));
}
return promiseGenerator().then(result => {
this.cache.set(key, result);
return result;
});
}
clear() {
this.cache.clear();
}
}
// Usage
const cache = new PromiseCache();
cache.get('user:1', () => fetchUser(1))
.then(user => console.log('User:', user));
// Second call returns cached result
cache.get('user:1', () => fetchUser(1))
.then(user => console.log('Cached user:', user));
9. Common Mistakes
9.1 Not Returning Promises
// ❌ Bad
fetchUser(1)
.then(user => {
getOrders(user.id); // Forgot to return!
})
.then(orders => {
// orders is undefined
console.log(orders);
});
// ✅ Good
fetchUser(1)
.then(user => {
return getOrders(user.id); // Return promise
})
.then(orders => {
console.log(orders);
});
9.2 Nested Promises
// ❌ Bad (callback hell with promises)
fetchUser(1)
.then(user => {
getOrders(user.id)
.then(orders => {
getOrderDetails(orders[0].id)
.then(details => {
console.log(details);
});
});
});
// ✅ Good (flat chain)
fetchUser(1)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => console.log(details));
9.3 Not Handling Errors
// ❌ Bad
fetchUser(1)
.then(user => console.log(user));
// ✅ Good
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error('Error:', error));
Summary
In this module, you learned:
- ✅ What Promises are and their three states
- ✅ Creating and consuming Promises
- ✅ Promise chaining for sequential operations
- ✅ Error handling with catch() and finally()
- ✅ Promise static methods: all, allSettled, race, any
- ✅ Common patterns: retry, sequential, queue
- ✅ Practical examples and real-world usage
- ✅ Common mistakes and how to avoid them
Next Steps
In Module 18, you'll learn about Async/Await, which makes working with Promises even more elegant.
Practice Exercises
- Convert callback-based functions to Promises
- Build a retry mechanism with exponential backoff
- Implement Promise.all() from scratch
- Create a rate-limited API client
- Build an image preloader with progress tracking
- Implement a promise-based timeout wrapper
- Create a caching layer for API requests
- Build a sequential task runner