Asynchronous Programming and HTTP
Learning Objectives
- You understand why asynchronous programming is needed in this course.
- You can use
asyncandawaitwithfetch. - 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.jsThe 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.
Async and concurrency
0 / 5 points
A program needs to fetch three independent JSON documents before it can build a report. When is Promise.all a good fit for that situation?
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.
Checking HTTP success
0 / 5 points
Your fetch call returns a Response, but the server may have replied with status 404 or 500. Why is checking response.ok a useful habit before treating the body as a successful result?
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.
Fetch and combine JSON
0 / 10 points
The assignment template comes with a small CLI project that should fetch JSON documents and combine their titles into one report.
Complete the functions in src/posts.js so that:
-
fetchJson(url)retrieves JSON and throws an error when the HTTP response is not OK, -
loadPostTitles(baseUrl, ids)loads several post titles concurrently, -
and
formatTitleList(titles)formats the titles as a numbered list in this exact style:1. First title2. Second title
The titles returned by loadPostTitles should stay in the same order as the given ids.
The provided tests already use stubs for fetch, so you do not need internet access to complete or test the assignment.
Use the tests while you work:
$ deno testThe CLI entry point is in main.js. The tests stub the network calls, so the assignment can be completed fully offline.
Do not change the imports in the provided test file.