Promises and Async/Await
Promise creation, Promise.withResolvers(), async/await, try/catch, error-first returns, top-level await, Promise.all(), Promise.allSettled(), Promise.race(), Promise.any(), anti-patterns to avoid.
Contents
Promise Fundamentals
Creating Promises
// Basic Promise
const basic = new Promise((resolve, reject) => {
setTimeout(() => {
if (success) {
resolve(result);
} else {
reject(new Error('Failed'));
}
}, 1000);
});
// ES2024: Promise.withResolvers()
const { promise, resolve, reject } = Promise.withResolvers();
// Control from outside
someEvent.on('complete', resolve);
someEvent.on('error', reject);
// Already resolved/rejected
const resolved = Promise.resolve(42);
const rejected = Promise.reject(new Error('Failed'));
// ES2025: Promise.try() — start a chain from a function that may
// throw synchronously; sync throws become rejections
const attempt = Promise.try(() => riskyOperation());Promise Chaining
Prefer async/await (see below) for most cases. Use
.then()for simple transforms or when you need the callback style.
fetchUser(userId)
.then(user => fetchPosts(user.id))
.then(posts => processPosts(posts))
.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => cleanup());Async/Await
Basic Usage
async function getUserData(userId) {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
}Error Handling Patterns
Let a rejection propagate unless this layer can recover, translate it, add useful context, or decide the final outcome. Catching, logging, and rethrowing at every layer commonly duplicates logs. Choose an owning boundary deliberately.
// Try/catch
async function withTryCatch() {
try {
const result = await riskyOperation();
return result;
} catch (error) {
return defaultValue;
}
}
// Error-first return (Go-style)
async function withErrorReturn() {
try {
const result = await riskyOperation();
return [null, result];
} catch (error) {
return [error, null];
}
}
const [error, data] = await withErrorReturn();
if (error) handleError(error);
else processData(data);
// Wrapper utility
function to(promise) {
return promise
.then(data => [null, data])
.catch(error => [error, null]);
}
const [err, user] = await to(fetchUser(id));Top-Level Await (ES2022)
// In ES modules (not CommonJS)
const config = await loadConfig();
const db = await connectDatabase(config);
export { db };Promise Combinators
Promise.all()
Wait for all inputs; reject as soon as one rejects. It does not cancel the other operations.
// Parallel execution
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
// With error handling
try {
const results = await Promise.all([taskA(), taskB(), taskC()]);
} catch (error) {
// Rejects when one input rejects; other operations keep running
console.error('One task failed:', error);
}Promise.allSettled()
Wait for all; get status of each.
const results = await Promise.allSettled([
fetchFromPrimary(),
fetchFromBackup(),
fetchFromCache()
]);
const successes = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failures = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);Promise.race()
First to settle (resolve or reject) wins. It does not cancel the losers. Use it only when losing operations may continue, or arrange cancellation through the operation's own protocol.
// fetch, AbortController, and timers are host APIs, not ECMAScript.
async function fetchWithTimeout(url, ms) {
const controller = new AbortController();
const timerId = setTimeout(
() => controller.abort(new Error('Timeout')),
ms,
);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timerId);
}
}
// First responder
const data = await Promise.race([
fetchFromServer1(),
fetchFromServer2()
]);Promise.any()
First to succeed wins; fails only if all fail.
// Fallback pattern
try {
const data = await Promise.any([
fetchFromPrimary(),
fetchFromSecondary(),
fetchFromTertiary()
]);
} catch (error) {
// AggregateError with all failures
console.error('All sources failed:', error.errors);
}Anti-Patterns
Unnecessary async
// ❌ Unnecessary wrapper
async function getUser(id) {
return await fetchUser(id);
}// ✅ Just return the promise
function getUser(id) {
return fetchUser(id);
}Sequential when parallel is possible
// ❌ Sequential (slow) — each await waits for the previous
const users = await fetchUsers();
const posts = await fetchPosts();
const comments = await fetchComments();// ✅ Parallel (fast)
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);Choose an owning error boundary
Ordinary rejection propagation is a valid async contract. Do not catch merely because an error can escape to the caller.
// Lower-level function deliberately propagates rejection.
async function buildReport() {
const data = await fetchData();
return transform(data);
}// A job boundary owns logging and the final failure outcome.
async function runReportJob() {
try {
await buildReport();
return { ok: true };
} catch (error) {
logger.error('Report job failed:', error);
return { ok: false, error };
}
}Mixing callbacks and promises
// ❌ Callback hell in async function
async function mixed() {
fs.readFile('file.txt', (err, data) => {
// This doesn't work with await
});
}// ✅ Promisify callbacks
import { promisify } from 'node:util';
const readFile = promisify(fs.readFile);
async function clean() {
const data = await readFile('file.txt');
return data;
}// ✅ Or better, use the promise-based API directly
import { readFile } from 'node:fs/promises';
const data = await readFile('file.txt', 'utf8');Creating promise inside loop
// ❌ Promises created but not awaited properly
async function bad() {
items.forEach(async item => {
await processItem(item); // This doesn't wait!
});
// Function returns before items are processed
}
// ✅ Use for...of for sequential
async function sequential() {
for (const item of items) {
await processItem(item);
}
}
// ✅ Use Promise.all for parallel
async function parallel() {
await Promise.all(items.map(item => processItem(item)));
}