JavaScript · Syntax · Intermediate

Promises and async/await

10 tasks

Handling asynchronous operations with Promise chains, async functions, and await.

What is a Promise: states, then chaining, and the microtask queue

#
A Promise represents a value that may not be available yet. It is in one of three states: **pending** (initial), **fulfilled** (resolved with a value), or **rejected** (failed with a reason). ```javascript const p = new Promise((resolve, reject) => { // executor runs synchronously if (Math.random() > 0.5) { resolve('success'); // transition to fulfilled } else { reject(new Error('unlucky')); // transition to rejected } }); p.then(value => console.log(value)) // 'success' .catch(err => console.log(err.message)); // 'unlucky' ``` **The microtask queue** Promise callbacks (`.then`, `.catch`, `.finally`) don't run immediately — they're placed in the *microtask queue* and run after the current synchronous code finishes, but before the next macrotask (setTimeout, setInterval, I/O): ```javascript console.log('1'); Promise.resolve().then(() => console.log('2')); console.log('3'); // Output: 1 3 2 // '2' runs after '3' because .then callbacks are microtasks ``` **Then chaining** `.then()` returns a new Promise. You can chain them to run steps in sequence: ```javascript fetchUser(id) .then(user => fetchPosts(user.id)) // must RETURN the next promise .then(posts => render(posts)) .catch(err => showError(err)); ``` **The most common chaining mistake: not returning** If you forget to `return` inside `.then`, the chain doesn't wait for the inner Promise — it resolves with `undefined` and the next `.then` fires immediately with `undefined`: ```javascript // Bug: missing return fetchUser(id) .then(user => { fetchPosts(user.id); // forgot 'return' ! }) .then(posts => { console.log(posts); // undefined — fetchPosts wasn't awaited }); // Fixed: fetchUser(id) .then(user => fetchPosts(user.id)) // arrow without braces implicitly returns .then(posts => console.log(posts)); ``` **Creating already-resolved/rejected Promises** ```javascript Promise.resolve(42).then(v => console.log(v)); // 42 Promise.reject(new Error('oops')).catch(e => console.log(e.message)); // 'oops' ``` These are useful for wrapping synchronous values in a Promise-compatible interface.

async/await: error handling, unhandled rejection, sequential vs parallel

#
`async` functions always return a Promise. `await` pauses execution inside an async function until the Promise settles — the surrounding code is not blocked. ```javascript async function getUser(id) { const user = await fetchUser(id); // waits for fetchUser const posts = await fetchPosts(user.id); // then waits for fetchPosts return { user, posts }; } // async function returns a Promise getUser(1).then(data => console.log(data)); ``` **Error handling with async/await** Use `try/catch` around `await` calls — it catches both synchronous throws and rejected Promises: ```javascript async function loadData(id) { try { const data = await fetchData(id); return process(data); } catch (err) { console.error('Failed:', err.message); return null; } finally { hideLoadingSpinner(); // always runs } } ``` **Unhandled rejection** If a rejected Promise has no `.catch` and no surrounding `try/catch`, it becomes an unhandled rejection. In Node.js this terminates the process; in browsers it fires `window.unhandledrejection`: ```javascript // Dangerous: no error handling async function bad() { const data = await fetchData(); // if this rejects, the error disappears silently return data; } // Better: always handle bad().catch(err => console.error(err)); ``` **Sequential vs parallel execution** Two awaits in a row run sequentially (second waits for first). To run in parallel, start both Promises *before* awaiting either: ```javascript // Sequential: total time = time(A) + time(B) async function sequential() { const a = await fetchA(); const b = await fetchB(); // only starts after fetchA finishes return [a, b]; } // Parallel: total time = max(time(A), time(B)) async function parallel() { const [a, b] = await Promise.all([fetchA(), fetchB()]); return [a, b]; } ``` **Mixing async/await with .then** You can mix them — `await` works on any thenable, and an async function's result can be chained with `.then`: ```javascript const result = await someAsyncFn().then(v => transform(v)); ``` But mixing styles in the same chain reduces readability. Prefer one style per chain.

Promise combinators: all, allSettled, race, any

#
**Promise.all** — all must succeed, else fail fast: ```javascript const [user, settings, feed] = await Promise.all([ fetchUser(id), fetchSettings(id), fetchFeed(id), ]); // If any one rejects, the whole thing rejects immediately. // The others keep running but their results are discarded. ``` **Promise.allSettled** — wait for all regardless of success/failure: ```javascript const results = await Promise.allSettled([ fetchUser(id), fetchSettings(id), ]); results.forEach(r => { if (r.status === 'fulfilled') console.log(r.value); else console.error(r.reason); }); // Use when you want all results, even partial ones. ``` **Promise.race** — settle as soon as the first one does: ```javascript const result = await Promise.race([ fetchData(), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000)), ]); // Common use: add a timeout to any async operation ``` **Promise.any** (ES2021) — settle on the first *fulfilled* result, ignore rejections: ```javascript const data = await Promise.any([ fetchFromCDN1(), fetchFromCDN2(), fetchFromCDN3(), ]); // Use for redundant requests: whichever CDN responds first wins. // Rejects with AggregateError only if ALL reject. ``` **Combinator comparison** ``` Promise.all - all succeed or fail fast. Result: array of values Promise.allSettled - wait for all. Result: array of {status, value/reason} Promise.race - first settled wins (success or failure) Promise.any - first fulfilled wins; fails only if all reject ``` **Sequential async iteration with for...of** When you need to process items one at a time (e.g., API rate limits), use `for...of` — `map` with `Promise.all` would fire all requests in parallel: ```javascript async function processSequentially(items) { const results = []; for (const item of items) { const result = await processItem(item); // waits for each results.push(result); } return results; } ```

Async patterns: retry, concurrency limit, timeout, queue, cache

#
Real-world async patterns that come up repeatedly. **Retry with exponential backoff** ```javascript async function retry(fn, maxAttempts = 3, delay = 500) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (err) { if (attempt === maxAttempts) throw err; await new Promise(r => setTimeout(r, delay * attempt)); } } } const data = await retry(() => fetchData(id), 3, 1000); // Tries up to 3 times, waiting 1s, 2s, 3s between attempts ``` **Parallel with concurrency limit** ```javascript async function mapWithLimit(items, fn, limit = 3) { const results = []; for (let i = 0; i < items.length; i += limit) { const batch = items.slice(i, i + limit); const batchResults = await Promise.all(batch.map(fn)); results.push(...batchResults); } return results; } // Process 100 URLs, 3 at a time const pages = await mapWithLimit(urls, fetchPage, 3); ``` **Timeout wrapper** ```javascript function withTimeout(promise, ms) { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms) ); return Promise.race([promise, timeout]); } const data = await withTimeout(fetchData(), 5000); ``` **Async queue (one at a time)** ```javascript function makeQueue() { let running = Promise.resolve(); return (fn) => { running = running.then(fn); return running; }; } const queue = makeQueue(); queue(() => save(a)); // these run one after the other queue(() => save(b)); queue(() => save(c)); ``` **Caching async results** ```javascript function asyncCache(fn) { const cache = new Map(); return async (key) => { if (!cache.has(key)) { cache.set(key, fn(key)); // store the Promise itself, not the result } return cache.get(key); }; } const cachedFetch = asyncCache(fetchUser); const [u1, u2] = await Promise.all([cachedFetch(1), cachedFetch(1)]); // fetchUser(1) called only once, even with concurrent requests ```
01

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"));
02

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));
03

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);
04

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));
05

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));
06

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));
07

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));
08

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);
09

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));
10

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);
}