CodeBlanks Logo Icon CodeBlanks

Summary - Async patterns

Combine for await, retry and debounce to write an API client.

Summary: API client

In this lesson we combine what we learned into an API client: retry, async generator and for await together.

The client works like this: given a list of URLs, each URL is fetched with retry (resilient to transient errors). The async generator yields each successfully fetched piece in turn. With for await each page is consumed. This is the skeleton of a real API client: reliability through retry, streaming through an async generator, consumption through for await. Together they form an error-resilient and memory-efficient data stream.

Combining the pieces

The client has three parts. First retry: async function retry(fn, maxAttempts) { ... }. Second the async generator: async function* fetchPages(urls) { for (const url of urls) { const data = await retry(async function(attempt) { ... }, 3); yield data; } }.

Third consumption with for await: for await (const page of fetchPages(urls)) { console.log(page); }. Each URL is fetched with retry; if the first attempt fails the second succeeds. retry passes the attempt number to the function, so if (attempt < 2) throw ... simulates a first-try failure. The generator yields the fetched data; for await takes each piece in turn and prints it. This is the complete pattern of resilient streaming.

Practice Write the API client

Write async function retry(fn, maxAttempts) (with try/catch). Write async function* fetchPages(urls): for each url const data = await retry(async function(attempt) { if (attempt < 2) throw new Error("Failed"); return url + ": attempt " + attempt; }, 3); yield data;. Inside async function main() write for await (const page of fetchPages(["a", "b"])) { console.log("Page: " + page); }. Call main().

Starter code

index.js
// Write a retry function (try/catch, maxAttempts loop)
// Yield pages in turn with an async generator
// Consume in a loop and print
// write here

Sample solution

index.js
async function retry(fn, maxAttempts) {
  let lastError;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn(attempt);
    } catch (e) {
      lastError = e;
    }
  }
  throw lastError;
}

async function* fetchPages(urls) {
  for (const url of urls) {
    const data = await retry(async function(attempt) {
      if (attempt < 2) throw new Error("Failed");
      return url + ": attempt " + attempt;
    }, 3);
    yield data;
  }
}

async function main() {
  const urls = ["a", "b"];
  for await (const page of fetchPages(urls)) {
    console.log("Page: " + page);
  }
}

main();

Expected output

Sayfa: a: deneme 2
Sayfa: b: deneme 2

Questions and answers

Review the lesson questions together with their answers. To test yourself, solve the lesson interactively.

1 Which pattern provides resilience to transient errors?

index.js
await retry(fn, 3)
  • setTimeout
  • debounce
  • throttle
  • retry Correct answer

Explanation: retry provides resilience by retrying a failed async job.

2 Match each piece to its role

Term Meaning
retry Resilience to transient errors
async function* Streams data
for await Consumes the streamed data
yield Produces a value

All lessons in Intermediate