Programming Foundations for LLM Applications

Asynchronous Programming and HTTP


Learning Objectives

  • You understand why asynchronous programming is needed in this course.
  • You can use async and await with fetch.
  • You know a basic pattern for running multiple independent tasks concurrently.

Asynchronous code and promises

Many programs in this course interact with things that do not return immediately. Reading files can take time. External APIs take time. Later, LLM services also take time. A program therefore needs a way to represent “work that has started but is not finished yet”.

JavaScript represents these delayed results using promises. In practice, the most readable way to work with them is usually async/await.

If you have not used promises before, a good way to think about them is this: a promise is a placeholder for a result that will become available later. await tells the program to pause at that point until the result is ready.

A first fetch example

The following program retrieves a single JSON document from a public API and prints one field from it.

const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const data = await response.json();

console.log(data.title);

Run the program with network access enabled:

$ deno run --allow-net app.js

The same pattern appears later when we call LLM APIs. The URL changes, but the logic is similar: make a request, await the response, parse JSON, and then continue with ordinary program logic.

It is worth noticing that this is still ordinary programming. The only special part is that the response is not available instantly. Once the JSON has been parsed, the rest of the code can treat the result like any other JavaScript object.

async functions

When asynchronous code is wrapped into a reusable function, the function itself becomes async.

const fetchPost = async (id) => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${id}`,
  );
  return await response.json();
};

Calling fetchPost(1) returns a promise. Using await fetchPost(1) waits for the result.

This is how asynchronous behavior spreads through a program. If one helper needs await, the helper itself becomes async, and any caller that wants the final result must also use await. That may sound inconvenient at first, but it keeps the program explicit about where delayed work happens.

Multiple tasks with Promise.all

If two asynchronous tasks do not depend on each other, they can often be started together.

const [post1, post2] = await Promise.all([
  fetchPost(1),
  fetchPost(2),
]);

console.log(post1.title);
console.log(post2.title);

This pattern is useful when a CLI program needs several independent results before producing its final output. The important design question is whether the tasks actually depend on each other. If the second task needs information from the first task, then Promise.all is the wrong tool. If they are independent, running them together is often clearer and faster.

For example, a later evaluation script might need to load several files or ask for several independent results before printing one report. That is a natural place for Promise.all.

Loading Exercise...

Handle failure explicitly

HTTP requests can fail because of network issues, invalid URLs, or server-side problems. A program should not silently assume that every request succeeds.

const fetchJson = async (url) => {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }
  return await response.json();
};

The same discipline becomes even more important later when responses come from paid or rate-limited APIs. A failing request is not only a technical inconvenience. It can also affect cost, user experience, and debugging time.

In larger programs, request code is often wrapped in try/catch so that the caller can decide what the user should see:

try {
  const post = await fetchJson("https://jsonplaceholder.typicode.com/posts/1");
  console.log(post.title);
} catch (error) {
  console.error(`Could not load the post: ${error.message}`);
}

This is a good general rule for asynchronous code in the course: delayed work should have explicit success and failure paths. Later, when the delayed work involves LLM APIs, this will become part of validation and fallback design rather than only networking.

Loading Exercise...

The programming exercise for this chapter uses stubbed fetch responses in the tests. That keeps the assignment deterministic and lets the learner focus on asynchronous control flow, error handling, and combining JSON results.

Loading Exercise...