Promises & Async/Await¶
Modern patterns for handling asynchronous JavaScript.
Promises Basics¶
A Promise represents a value that may be available now, later, or never.
// Creating a Promise
const promise = new Promise((resolve, reject) => {
// Async operation
setTimeout(() => {
const success = true;
if (success) {
resolve('Data loaded');
} else {
reject(new Error('Failed to load'));
}
}, 1000);
});
// Using the Promise
promise
.then(data => console.log(data))
.catch(error => console.error(error));
Promise States¶
Once settled (fulfilled or rejected), a promise cannot change state.
Promise Methods¶
Promise.resolve() and Promise.reject()¶
// Create immediately resolved promise
const resolved = Promise.resolve('value');
// Create immediately rejected promise
const rejected = Promise.reject(new Error('error'));
// Useful for wrapping values
function maybeAsync(value) {
if (cached.has(value)) {
return Promise.resolve(cached.get(value));
}
return fetchFromServer(value);
}
Promise.all()¶
Wait for all promises, fail if any fails.
async function fetchUserData(userId) {
const [user, posts, comments] = await Promise.all([
fetch(`/users/${userId}`).then(r => r.json()),
fetch(`/posts?userId=${userId}`).then(r => r.json()),
fetch(`/comments?userId=${userId}`).then(r => r.json()),
]);
return { user, posts, comments };
}
// If any promise rejects, the whole thing rejects
try {
const data = await Promise.all([
Promise.resolve(1),
Promise.reject(new Error('Oops')),
Promise.resolve(3),
]);
} catch (error) {
console.error(error); // Error: Oops
}
Promise.allSettled()¶
Wait for all, get results regardless of success/failure.
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/broken'), // This fails
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Request ${index} succeeded:`, result.value);
} else {
console.log(`Request ${index} failed:`, result.reason);
}
});
// Output:
// Request 0 succeeded: Response {...}
// Request 1 succeeded: Response {...}
// Request 2 failed: Error: ...
Promise.race()¶
Resolve/reject with the first settled promise.
// Timeout pattern
async function fetchWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
);
return Promise.race([fetchPromise, timeoutPromise]);
}
Promise.any()¶
Resolve with the first fulfilled promise (ignores rejections).
// Try multiple servers, use first successful
const result = await Promise.any([
fetch('https://server1.com/data'),
fetch('https://server2.com/data'),
fetch('https://server3.com/data'),
]);
// Only rejects if ALL promises reject
Async/Await¶
Syntactic sugar over Promises for cleaner code.
Basic Usage¶
// Without async/await
function fetchUser(id) {
return fetch(`/users/${id}`)
.then(response => response.json())
.then(user => {
return fetch(`/posts?userId=${user.id}`);
})
.then(response => response.json());
}
// With async/await
async function fetchUser(id) {
const response = await fetch(`/users/${id}`);
const user = await response.json();
const postsResponse = await fetch(`/posts?userId=${user.id}`);
const posts = await postsResponse.json();
return { user, posts };
}
Error Handling¶
// try/catch works with await
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch:', error);
throw error; // Re-throw if needed
}
}
// Or handle at call site
const data = await fetchData().catch(error => {
console.error(error);
return null;
});
Parallel vs Sequential¶
// Sequential (slower)
async function sequential() {
const user = await fetchUser(); // Wait...
const posts = await fetchPosts(); // Then wait...
const comments = await fetchComments(); // Then wait...
return { user, posts, comments };
}
// Parallel (faster)
async function parallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
return { user, posts, comments };
}
Conditional Parallel¶
async function fetchUserData(userId, options = {}) {
const promises = [fetchUser(userId)];
if (options.includePosts) {
promises.push(fetchPosts(userId));
}
if (options.includeComments) {
promises.push(fetchComments(userId));
}
const results = await Promise.all(promises);
return {
user: results[0],
posts: options.includePosts ? results[1] : [],
comments: options.includeComments ? results[options.includePosts ? 2 : 1] : [],
};
}
Common Patterns¶
Retry with Backoff¶
async function fetchWithRetry(url, options = {}) {
const { retries = 3, backoff = 1000 } = options;
for (let attempt = 0; attempt < retries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (attempt === retries - 1) throw error;
const delay = backoff * Math.pow(2, attempt);
await new Promise(r => setTimeout(r, delay));
}
}
}
Timeout Wrapper¶
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([promise, timeout]);
}
// Usage
const data = await withTimeout(fetch('/api/slow'), 5000);
Rate Limiting¶
class RateLimiter {
private queue: (() => void)[] = [];
private running = 0;
constructor(private maxConcurrent: number) {}
async add<T>(fn: () => Promise<T>): Promise<T> {
if (this.running >= this.maxConcurrent) {
await new Promise<void>(resolve => this.queue.push(resolve));
}
this.running++;
try {
return await fn();
} finally {
this.running--;
const next = this.queue.shift();
if (next) next();
}
}
}
// Usage
const limiter = new RateLimiter(5);
const results = await Promise.all(
urls.map(url => limiter.add(() => fetch(url)))
);
Batching¶
async function processBatch<T, R>(
items: T[],
batchSize: number,
fn: (item: T) => Promise<R>
): Promise<R[]> {
const results: R[] = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map(fn));
results.push(...batchResults);
}
return results;
}
// Process 100 items, 10 at a time
const results = await processBatch(items, 10, processItem);
Sequential Processing¶
// When order matters
async function processSequentially<T, R>(
items: T[],
fn: (item: T) => Promise<R>
): Promise<R[]> {
const results: R[] = [];
for (const item of items) {
const result = await fn(item);
results.push(result);
}
return results;
}
// Or with reduce
async function processSequentially2<T, R>(
items: T[],
fn: (item: T) => Promise<R>
): Promise<R[]> {
return items.reduce(async (accPromise, item) => {
const acc = await accPromise;
const result = await fn(item);
return [...acc, result];
}, Promise.resolve([] as R[]));
}
Caching¶
const cache = new Map<string, Promise<Response>>();
async function fetchCached(url: string): Promise<Response> {
if (!cache.has(url)) {
// Store the promise, not the result
cache.set(url, fetch(url));
}
try {
return await cache.get(url)!.then(r => r.clone());
} catch (error) {
cache.delete(url); // Remove failed requests
throw error;
}
}
Error Handling Patterns¶
Error Types¶
class NetworkError extends Error {
constructor(message: string, public status?: number) {
super(message);
this.name = 'NetworkError';
}
}
async function fetchWithErrorTypes(url: string) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new NetworkError(`HTTP error`, response.status);
}
return await response.json();
} catch (error) {
if (error instanceof NetworkError) {
// Handle network errors
console.error('Network error:', error.status);
} else if (error instanceof SyntaxError) {
// JSON parse error
console.error('Invalid JSON');
} else {
throw error;
}
}
}
Graceful Degradation¶
async function fetchWithFallback<T>(
primary: () => Promise<T>,
fallback: () => Promise<T>
): Promise<T> {
try {
return await primary();
} catch (error) {
console.warn('Primary failed, using fallback:', error);
return await fallback();
}
}
// Usage
const data = await fetchWithFallback(
() => fetch('/api/v2/data').then(r => r.json()),
() => fetch('/api/v1/data').then(r => r.json())
);
Common Mistakes¶
// Bad: forEach doesn't wait for async
items.forEach(async item => {
await processItem(item); // Doesn't actually wait!
});
// Good: Use for...of
for (const item of items) {
await processItem(item);
}
// Or Promise.all for parallel
await Promise.all(items.map(item => processItem(item)));
// Bad: Catching too broadly
try {
const data = await riskyOperation();
processData(data); // Errors here are also caught!
} catch (error) {
// Can't tell which operation failed
}
// Good: Narrow try/catch
let data;
try {
data = await riskyOperation();
} catch (error) {
throw new Error('Failed to fetch data');
}
processData(data);
// Bad: Not awaiting in return
async function bad() {
return fetch('/api/data').json(); // Missing await!
}
// Good
async function good() {
const response = await fetch('/api/data');
return response.json(); // Still need to handle this promise
}