Files
What you'll learn
- Write async functions and type what their promises carry
- Run independent work at once with Promise.all, and keep going past failures with Promise.allSettled
- Race a promise against a timer, and retry a failing operation with try and catch around await
Async code
A server spends most of its time waiting: for the database, for another service, for a file. JavaScript does not block while it waits; it hands back a promise, a value that will arrive later, and gets on with other requests. Nearly every handler in NestJS and Hono is async for this reason. Waiting well is a skill of its own: waiting for three things one after another takes three times as long as waiting for them together, one failure should not always sink the rest, and a service that never answers must not hold a request forever.
Promises and async functions
An async function always returns a promise, and await pauses that function (and only that function) until a promise settles:
async function ownerName(id: number): Promise<string> {
const owner = await fetchOwner(id); // Promise<Owner> → Owner
return owner.name; // wrapped: Promise<string>
}
The return type names what the promise carries, Promise<string>, and await unwraps it. A promise settles once: it is fulfilled with a value or rejected with an error. A rejection surfaces at await as a thrown error, so try/catch around await handles it like any other exception.
One after another, or all at once
const a = await fetchOwner(1); // wait...
const b = await fetchOwner(2); // ...then wait again
That takes the time of both, and the second request only starts when the first finishes. When the two do not depend on each other, start both and then wait:
const [a, b] = await Promise.all([fetchOwner(1), fetchOwner(2)]);
Promise.all takes promises that are already running and fulfils with their values in the order given, whatever order they finished in. It rejects as soon as any one rejects, with that error. Mapping a list of ids to calls, ids.map((id) => fetchOwner(id)), starts every call before anything waits. A for loop with await inside does not: each call waits for the one before.
When some may fail
Sometimes a partial answer is better than none: show the owners that loaded and say which did not. Promise.allSettled never rejects; it waits for every promise and gives one result per promise, in order:
const results = await Promise.allSettled(ids.map((id) => fetchOwner(id)));
// [{ status: 'fulfilled', value: owner }, { status: 'rejected', reason: error }, ...]
status is a literal type, so checking it narrows the result, the discriminated union from lesson 1 again.
Deadlines
A call to a service that has hung will wait as long as the connection lets it. Promise.race settles with whichever promise settles first, so racing the call against a timer that rejects puts a limit on it:
const deadline = new Promise<never>((_, reject) => setTimeout(() => reject(new Error('too slow')), 1000));
const owner = await Promise.race([fetchOwner(1), deadline]);
new Promise((resolve, reject) => …) builds a promise by hand, which is how callback APIs like setTimeout become promises. Promise<never> says this one never fulfils with a value. Clearing the timer once the race is decided (.finally(() => clearTimeout(timer))) stops it running for nothing.
Trying again
A failed call to a flaky service often works the second time. A retry is a loop around try/catch: return on the first success, remember the error on each failure, and throw the last one when the attempts run out. return await attempt() inside try matters: without the await, the promise is returned before it settles, and its rejection escapes the catch.
Your task
The work is in feeder.ts; the spec hands each function a fake API that counts how many fetches run at once.
loadAll(ids, fetchCat)fetches every cat at the same time and answers them in the order of the ids; if any fetch fails, it fails with that error.loadAvailable(ids, fetchCat)fetches every cat at the same time and answers{ found, missing }: the cats that came back and the ids that failed, each in the order of the ids.withTimeout(promise, ms)settles like the promise, unlessmsmilliseconds pass first; then it rejects with anErrorsaying"timed out after <ms>ms".retry(attempt, times)callsattempt()until it succeeds, at mosttimestimes, and answers the first success, or fails with the last error.
When it fails
- "fetches every cat at the same time" sees a peak of 1: the fetches were awaited one by one in a loop.
- "answers in the order of the ids" gets Milo first: results were collected as they arrived.
Promise.allkeeps the order for you. - "fails with the last error" gets
failure 1: the first error was kept instead of the last, or the loop stopped early. - A test times out: a promise that never settles is being awaited without the race.
Remember
asyncfunctions return promises;awaitunwraps them and turns rejections into throws.- Start independent work together and
Promise.allit; results come back in the order given. Promise.allSettlednever rejects and reports each outcome;Promise.racetakes the first to settle.- A retry is
try/catchin a loop, withreturn awaitso the catch sees the rejection.
Stuck? Show a hint
Promise.all(ids.map((id) => fetchCat(id))) starts every fetch before waiting for any, and keeps the order of the ids. Promise.allSettled gives one { status, value } or { status, reason } per promise, in order. Promise.race([promise, timeout]) settles with whichever settles first; the timeout is a new Promise that rejects from a setTimeout. For retry, a loop with return await attempt() inside try, and the error kept from the catch.
Press Run tests to start the app. Its log appears here.Tests
- feeder.spec.tsrun to see its tests