JavaScript Core
Promises
Manage asynchronous operations with elegance
Intermediate
javascript async promises
Definition
A Promise is an object representing the eventual completion or failure of an asynchronous operation. It allows you to attach callbacks to handle the eventual success value or failure reason, providing a cleaner alternative to callback-based async code.
Promise States
const promise = new Promise((resolve, reject) => {
// Promise starts in "pending" state
if (success) {
resolve(value); // Transition to "fulfilled"
} else {
reject(error); // Transition to "rejected"
}
});
// States:
// 1. Pending - Initial state, not yet completed
// 2. Fulfilled - Operation completed successfully
// 3. Rejected - Operation failed
// 4. Settled - Either fulfilled or rejected
Creating Promises
Basic Promise Constructor
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
const data = { id: 1, name: 'John' };
resolve(data);
}, 1000);
});
fetchData
.then(data => console.log(data))
.catch(error => console.error(error));
Wrapping Callback APIs
// Convert callback-based API to Promise
function readFilePromise(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// Usage
readFilePromise('./file.txt')
.then(content => console.log(content))
.catch(error => console.error('Failed to read:', error));
Promisify Pattern
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
}
// Usage
const readFileAsync = promisify(fs.readFile);
const statAsync = promisify(fs.stat);
Chaining
Then Chaining
fetchUser(1)
.then(user => {
console.log('User:', user);
return fetchOrders(user.id);
})
.then(orders => {
console.log('Orders:', orders);
return calculateTotal(orders);
})
.then(total => {
console.log('Total:', total);
})
.catch(error => {
console.error('Error in chain:', error);
});
Return Values
// Returning a value
Promise.resolve(5)
.then(x => x * 2) // returns 10
.then(x => x + 1) // returns 11
.then(console.log); // 11
// Returning a Promise
Promise.resolve(1)
.then(x => Promise.resolve(x * 2)) // returns Promise
.then(x => x + 1) // unwraps to 3
.then(console.log);
Error Handling
Catch Block
fetchUser(1)
.then(user => fetchOrders(user.id))
.then(orders => processOrders(orders))
.catch(error => {
// Catches any rejection in the chain
console.error('Operation failed:', error);
return defaultOrders;
})
.then(orders => displayOrders(orders));
Error Recovery
fetchData()
.catch(error => {
console.warn('Primary source failed, trying backup...');
return fetchBackupData();
})
.then(data => {
// Uses either primary or backup data
processData(data);
})
.catch(error => {
// Both sources failed
showErrorMessage('Data unavailable');
});
Finally Block
showLoadingSpinner();
fetchData()
.then(data => updateUI(data))
.catch(error => showError(error))
.finally(() => {
// Always runs, success or failure
hideLoadingSpinner();
});
Promise Combinators
Promise.all
// Wait for all promises to fulfill
const promises = [
fetchUser(1),
fetchPosts(),
fetchSettings()
];
Promise.all(promises)
.then(([user, posts, settings]) => {
console.log('All loaded:', { user, posts, settings });
})
.catch(error => {
// If ANY promise rejects
console.error('At least one failed:', error);
});
// With error handling per promise
Promise.all(
promises.map(p => p.catch(err => ({ error: err.message })))
)
.then(results => {
// All results, including errors
});
Promise.allSettled
// Wait for all promises to settle (success or failure)
const promises = [
fetchUser(1),
fetchPosts(), // Might fail
fetchSettings()
];
Promise.allSettled(promises)
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.error('Failed:', result.reason);
}
});
});
// Results array:
// [
// { status: 'fulfilled', value: ... },
// { status: 'rejected', reason: ... },
// { status: 'fulfilled', value: ... }
// ]
Promise.race
// Returns the first settled promise
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), 5000);
});
Promise.race([fetchData(), timeout])
.then(data => console.log('Success:', data))
.catch(error => console.error('Failed:', error.message));
Promise.any
// Returns first fulfilled promise (ignores rejections)
const mirrors = [
fetch('https://mirror1.example.com'),
fetch('https://mirror2.example.com'),
fetch('https://mirror3.example.com')
];
Promise.any(mirrors)
.then(response => console.log('First successful:', response))
.catch(error => {
// AggregateError if all rejected
console.error('All mirrors failed:', error.errors);
});
Static Methods
Quick Creation
// Resolved promise
const resolved = Promise.resolve(42);
// Rejected promise
const rejected = Promise.reject(new Error('Failed'));
// From iterable
const promises = [1, 2, 3].map(n => Promise.resolve(n * 2));
Anti-Patterns
The Promise Constructor Antipattern
// Wrong - Unnecessary wrapping
function getUser(id) {
return new Promise((resolve, reject) => {
fetchUser(id)
.then(user => resolve(user))
.catch(err => reject(err));
});
}
// Correct - Just return the promise
function getUser(id) {
return fetchUser(id);
}
Nested Promises
// Wrong - Promise pyramid of doom
fetchUser(1)
.then(user => {
return fetchOrders(user.id)
.then(orders => {
return calculateTotal(orders)
.then(total => {
return { user, orders, total };
});
});
});
// Correct - Flat chain
fetchUser(1)
.then(user =>
fetchOrders(user.id)
.then(orders => ({ user, orders }))
)
.then(({ user, orders }) =>
calculateTotal(orders)
.then(total => ({ user, orders, total }))
);
Forgetting to Return
// Wrong - Missing return creates broken chain
fetchUser(1)
.then(user => {
fetchOrders(user.id); // Missing return!
})
.then(orders => {
// orders is undefined!
});
// Correct
fetchUser(1)
.then(user => fetchOrders(user.id))
.then(orders => processOrders(orders));
Modern Patterns
Sequential Execution
// Sequential processing with reduce
const urls = ['/api/1', '/api/2', '/api/3'];
const results = await urls.reduce(async (promise, url) => {
const results = await promise;
const response = await fetch(url);
const data = await response.json();
return [...results, data];
}, Promise.resolve([]));
Parallel with Concurrency Limit
async function* parallelWithLimit(tasks, limit) {
const executing = new Set();
for (const task of tasks) {
const promise = task().then(result => {
executing.delete(promise);
return result;
});
executing.add(promise);
if (executing.size >= limit) {
yield await Promise.race(executing);
}
}
yield* executing;
}
// Usage: Process with max 3 concurrent
for await (const result of parallelWithLimit(tasks, 3)) {
console.log(result);
}
Retry Logic
function retry(fn, maxAttempts = 3) {
return function(...args) {
return new Promise((resolve, reject) => {
const attempt = (n) => {
fn(...args)
.then(resolve)
.catch(error => {
if (n === maxAttempts) {
reject(error);
} else {
setTimeout(() => attempt(n + 1), 1000 * n);
}
});
};
attempt(1);
});
};
}
const fetchWithRetry = retry(fetchData, 3);
fetchWithRetry('/api/data').then(console.log);
Key Takeaway
Promises provide a robust foundation for async programming in JavaScript. Master chaining with .then(), handle errors with .catch(), use Promise.all() for parallel operations, and avoid common antipatterns like unnecessary wrapping. For most modern code, async/await provides cleaner syntax, but understanding Promises is essential for debugging and advanced patterns.