Skip to main content

Async Problems

The hardest set: promises, async/await, and the patterns you will use daily when talking to APIs. Each problem simulates network calls with timers, so everything runs in a plain browser console or Node — no server needed.

1. Build a delay Function

Write delay(ms) that returns a promise which resolves after ms milliseconds, then use it to pause between two log statements.

Example
console.log("Order placed");
await delay(1000);
console.log("Order confirmed"); // appears ~1 second later
Solution
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const confirmOrder = async () => {
console.log("Order placed");
await delay(1000);
console.log("Order confirmed");
};

confirmOrder();

setTimeout is callback-based, so we wrap it in a promise: the promise's resolve becomes the timer's callback. This tiny helper is the foundation for every other problem on this page — it turns "wait some time" into something you can await.

2. Sequential vs Parallel

Rizwan's dashboard needs a user profile and their orders. Each fake API call takes 1 second. Load both sequentially, then in parallel, and compare the total time.

Example
const fetchUser = () => delay(1000).then(() => ({ name: "Rizwan" }));
const fetchOrders = () => delay(1000).then(() => [{ id: 1, total: 6120 }]);

// sequential: ~2000ms
// parallel: ~1000ms
Solution
const loadSequential = async () => {
console.time("sequential");
const user = await fetchUser();
const orders = await fetchOrders();
console.timeEnd("sequential"); // sequential: ~2000ms
return { user, orders };
};

const loadParallel = async () => {
console.time("parallel");
const [user, orders] = await Promise.all([fetchUser(), fetchOrders()]);
console.timeEnd("parallel"); // parallel: ~1000ms
return { user, orders };
};

In the sequential version, the second request does not even start until the first finishes — the waits add up. Promise.all starts both requests immediately and waits for both, so the total time is the slowest single request, not the sum. Rule of thumb: await one by one only when a later call needs the result of an earlier one.

3. Promise Timeout Wrapper

An API sometimes hangs. Write withTimeout(promise, ms) that rejects with "Request timed out" if the promise takes longer than ms milliseconds.

Example
const slowApi = delay(3000).then(() => "profile data");

withTimeout(slowApi, 1500)
.then(console.log)
.catch((err) => console.log(err.message));
// after 1.5s logs: "Request timed out"
Solution
const withTimeout = (promise, ms) =>
Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Request timed out")), ms)
),
]);

Promise.race settles with whichever promise finishes first. We race the real work against a promise that only knows how to fail after ms — if the API answers in time it wins the race, otherwise the timeout rejection wins. In production code you would also cancel the underlying request with an AbortController, since losing the race does not stop the original operation.

4. Retry with Backoff

Zakariya's service occasionally returns errors under load. Write retry(fn, retries, wait) that re-runs a failing async function, doubling the wait between attempts, and gives up after the last attempt.

Example
let calls = 0;
const flakyApi = async () => {
calls++;
if (calls < 3) throw new Error("Service unavailable");
return "Connected";
};

retry(flakyApi, 3, 500).then(console.log);
// Attempt 1 failed, retrying in 500ms
// Attempt 2 failed, retrying in 1000ms
// Connected
Solution
const retry = async (fn, retries = 3, wait = 500) => {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === retries) throw err;
console.log(`Attempt ${attempt} failed, retrying in ${wait}ms`);
await delay(wait);
wait *= 2;
}
}
};

The loop attempts the call, and a success returns immediately. On failure we wait, double the wait (exponential backoff: 500ms, 1000ms, 2000ms...), and try again — giving a struggling server breathing room instead of hammering it. On the final attempt the error is re-thrown so the caller still sees the real failure.

5. Promise.all Error Handling

You load three user profiles at once. With Promise.all, one missing user kills the whole batch. Load them so the successful profiles still come through, and the failures are reported separately.

Example
const getUser = (name, shouldFail = false) =>
shouldFail
? delay(300).then(() => Promise.reject(new Error(`${name} not found`)))
: delay(300).then(() => ({ name }));

// Expected output:
// Loaded: Rizwan, Hafsa
// Failed: Ghost not found
Solution
const loadProfiles = async () => {
const results = await Promise.allSettled([
getUser("Rizwan"),
getUser("Ghost", true),
getUser("Hafsa"),
]);

const loaded = results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value.name);
const failed = results
.filter((r) => r.status === "rejected")
.map((r) => r.reason.message);

console.log(`Loaded: ${loaded.join(", ")}`);
console.log(`Failed: ${failed.join(", ")}`);
};

loadProfiles();

Promise.all is all-or-nothing: the first rejection rejects the combined promise and the other results are lost. Promise.allSettled never rejects — it waits for everything and hands back one entry per promise, either { status: "fulfilled", value } or { status: "rejected", reason }. Use all when a single failure makes the whole batch useless, and allSettled when partial results are still valuable.

6. Promisify a Callback API

A legacy library uses Node-style callbacks: the last argument is callback(err, result). Write promisify(fn) so the function can be used with async/await.

Example
const getUserLegacy = (id, callback) => {
setTimeout(() => {
if (id === 101) callback(null, { id: 101, name: "Zakariya" });
else callback(new Error("User not found"));
}, 300);
};

const getUser = promisify(getUserLegacy);
await getUser(101); // { id: 101, name: "Zakariya" }
await getUser(999); // throws Error: User not found
Solution
const promisify =
(fn) =>
(...args) =>
new Promise((resolve, reject) => {
fn(...args, (err, result) => (err ? reject(err) : resolve(result)));
});

The returned function forwards all arguments to the original and appends one extra: a callback that translates the Node convention into promise language — an error rejects, a result resolves. Once wrapped, the legacy function composes with everything else on this page: withTimeout(getUser(101), 1000), retry(() => getUser(101)), and plain await all just work. Node ships this exact utility as util.promisify.