JavaScript · Syntax · Intermediate
Promises and async/await
Handling asynchronous operations with Promise chains, async functions, and await.
Quick topic start and explanations before exercises (exercises below):
async/await: error handling, unhandled rejection, sequential vs parallel
#Promise combinators: all, allSettled, race, any
#Async patterns: retry, concurrency limit, timeout, queue, cache
#Exercises:
Create a Promise
#Write a function delay(ms) that returns a Promise that resolves after the given number of milliseconds.
function delay(ms) {
}
delay(1000).then(() => console.log("Done after 1 second"));
Solution
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
delay(1000).then(() => console.log("Done after 1 second"));
Promise with value
#Write a function fetchUser(id) that returns a Promise that resolves with a mock user object { id, name } after 500ms. If id is 0, reject with an error message 'Invalid ID'.
function fetchUser(id) {
}
fetchUser(1).then(user => console.log(user));
fetchUser(0).catch(err => console.log(err));
Solution
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 0) {
reject('Invalid ID');
} else {
resolve({ id, name: `User ${id}` });
}
}, 500);
});
}
fetchUser(1).then(user => console.log(user));
fetchUser(0).catch(err => console.log(err));
async/await basic
#Rewrite the following Promise chain using async/await. The function should fetch a user, then log their name. Handle errors with try/catch. Original: fetchUser(id).then(u => console.log(u.name)).catch(e => console.log('Error:', e))
// Assume fetchUser(id) returns a Promise (from the previous exercise)
async function showUserName(id) {
}
showUserName(1);
showUserName(0);
Solution
async function showUserName(id) {
try {
const user = await fetchUser(id);
console.log(user.name);
} catch (e) {
console.log('Error:', e);
}
}
showUserName(1);
showUserName(0);
Promise.all
#Write an async function fetchAll(ids) that takes an array of user IDs, fetches all users in parallel using Promise.all, and returns the array of user objects.
// Assume fetchUser(id) is available
async function fetchAll(ids) {
}
fetchAll([1, 2, 3]).then(users => console.log(users));
Solution
async function fetchAll(ids) {
return Promise.all(ids.map(id => fetchUser(id)));
}
fetchAll([1, 2, 3]).then(users => console.log(users));
Sequential async operations
#Write an async function processUsers(ids) that fetches users one by one (sequentially, not in parallel) and returns an array of their names.
async function processUsers(ids) {
}
processUsers([1, 2, 3]).then(names => console.log(names));
Solution
async function processUsers(ids) {
const names = [];
for (const id of ids) {
const user = await fetchUser(id);
names.push(user.name);
}
return names;
}
processUsers([1, 2, 3]).then(names => console.log(names));
Promise.allSettled
#Write an async function fetchWithResults(ids) that tries to fetch all users in parallel and returns an array of objects { id, user } for successful requests and { id, error } for failed ones. Use Promise.allSettled.
async function fetchWithResults(ids) {
}
fetchWithResults([1, 0, 2]).then(results => console.log(results));
Solution
async function fetchWithResults(ids) {
const results = await Promise.allSettled(ids.map(id => fetchUser(id)));
return results.map((result, i) => {
if (result.status === 'fulfilled') {
return { id: ids[i], user: result.value };
} else {
return { id: ids[i], error: result.reason };
}
});
}
fetchWithResults([1, 0, 2]).then(results => console.log(results));
Async with timeout
#Write an async function withTimeout(promise, ms) that races the given promise against a timeout. If the promise doesn't resolve within ms milliseconds, reject with 'Timeout'.
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function withTimeout(promise, ms) {
}
// Test: a slow operation that takes 2 seconds
const slow = delay(2000).then(() => 'done');
withTimeout(slow, 1000)
.then(r => console.log(r))
.catch(e => console.log(e));
Solution
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject('Timeout'), ms)
);
return Promise.race([promise, timeout]);
}
const slow = delay(2000).then(() => 'done');
withTimeout(slow, 1000)
.then(r => console.log(r))
.catch(e => console.log(e));
Retry on failure
#Write an async function retry(fn, attempts) that calls fn() (which returns a Promise) up to `attempts` times. If fn succeeds, return the result. If all attempts fail, throw the last error.
async function retry(fn, attempts) {
}
let callCount = 0;
const flaky = () => new Promise((resolve, reject) => {
callCount++;
if (callCount < 3) reject(new Error(`Attempt ${callCount} failed`));
else resolve('Success!');
});
retry(flaky, 5).then(console.log).catch(console.error);
Solution
async function retry(fn, attempts) {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (e) {
lastError = e;
}
}
throw lastError;
}
let callCount = 0;
const flaky = () => new Promise((resolve, reject) => {
callCount++;
if (callCount < 3) reject(new Error(`Attempt ${callCount} failed`));
else resolve('Success!');
});
retry(flaky, 5).then(console.log).catch(console.error);
Promise chain vs async/await
#Rewrite the following Promise chain as an async function. The original fetches a user, then fetches their posts, then returns the titles of all posts. Original (assume getUserPosts(userId) returns a Promise of post array): getUser(id).then(u => getUserPosts(u.id)).then(posts => posts.map(p => p.title))
// Assume getUser(id) and getUserPosts(userId) return Promises
async function getUserPostTitles(id) {
}
getUserPostTitles(1).then(titles => console.log(titles));
Solution
async function getUserPostTitles(id) {
const user = await getUser(id);
const posts = await getUserPosts(user.id);
return posts.map(p => p.title);
}
getUserPostTitles(1).then(titles => console.log(titles));
Async error propagation
#Write an async function pipeline(id) that: 1) fetches a user, 2) if the user's name starts with 'A', throws an error 'Name starts with A', 3) otherwise logs the user name. Wrap the entire call to pipeline() in a try/catch that logs any error.
async function pipeline(id) {
}
try {
await pipeline(1);
} catch (e) {
console.log('Caught:', e.message || e);
}
Solution
async function pipeline(id) {
const user = await fetchUser(id);
if (user.name.startsWith('A')) {
throw new Error('Name starts with A');
}
console.log(user.name);
}
try {
await pipeline(1);
} catch (e) {
console.log('Caught:', e.message || e);
}