Unit 3 · Scripting & Storage

Lesson · Unit 3 · 9 min read

Async, fetch & APIs, how JavaScript talks to the network.

JavaScript runs on one thread. If a network call blocked, your whole page would freeze. The async machinery is how JS does long operations without locking up — and it's how every modern app talks to a server.

Section · 01

Why async exists

JavaScript is single-threaded. There’s one thread of execution per page. If you ran a 5-second database query synchronously, the page would be completely frozen for 5 seconds — no clicks, no animations, no scrolls. Unacceptable.

The fix: anything that takes meaningful time (network requests, file I/O, timers) is asynchronous. You ask for the result, you get a placeholder, you carry on. When the result arrives, your code resumes from where it left off.

Section · 02

Callbacks — the old way

setTimeout(() => {
  console.log("3 seconds later");
}, 3000);

// Old-school API request style — looks like this even in 2024 in some libraries
loadUser(123, function(err, user) {
  if (err) return console.error(err);
  loadOrders(user.id, function(err, orders) {
    if (err) return console.error(err);
    renderOrders(orders);
  });
});

Callbacks work but they nest fast. Three or four levels deep and you’re in “callback hell.” ES2015 introduced promises to fix this.

Section · 03

Promises — the modern way

A Promiseis an object that represents eventual completion of an async operation. It’s a placeholder for a future value.

fetch("/api/user/123")
  .then((response) => response.json())
  .then((user) => {
    console.log(user.name);
  })
  .catch((err) => {
    console.error(err);
  });

Each .then() takes a function that runs when the previous step resolves. .catch() runs if anything in the chain threw. No nesting.

Section · 04

async / await — promises that look synchronous

The async keyword on a function lets you use await inside it. awaitpauses until a promise resolves and gives you the result. The code reads top-down even though it’s async.

async function loadUser(id) {
  try {
    const response = await fetch(`/api/user/${id}`);
    if (!response.ok) {
      throw new Error("HTTP " + response.status);
    }
    const user = await response.json();
    console.log(user.name);
    return user;
  } catch (err) {
    console.error("Failed to load user:", err);
  }
}

loadUser(123);

Same behavior as the .then() chain, way easier to read. try/catch handles errors like it would in synchronous code. This is the modern default.

Section · 05

fetch — the browser's HTTP client

// GET (default)
const res = await fetch("/api/posts");
const posts = await res.json();

// POST with JSON body
const res = await fetch("/api/posts", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "Hi", body: "First post" }),
});
const created = await res.json();

// Send a form's data
const formData = new FormData(formElement);
const res = await fetch("/api/upload", {
  method: "POST",
  body: formData,
});

Checking for errors

A 404 or 500 isn’t a thrown exception in fetch— it’s a successful response with a bad status. Always check res.ok or res.status:

const res = await fetch("/api/posts");
if (!res.ok) {
  throw new Error(`Request failed: ${res.status}`);
}
const data = await res.json();

Section · 06

What an API actually is

An API (Application Programming Interface) is a way for one program to ask another to do something. On the web, almost all APIs are HTTP APIs: you make an HTTP request, you get a (usually) JSON response.

Common patterns you'll see:

GET    /api/posts            → list of posts
GET    /api/posts/42         → post with id 42
POST   /api/posts            → create a new post (body has the data)
PATCH  /api/posts/42         → update fields on post 42
DELETE /api/posts/42         → delete post 42

This is the REST pattern (Representational State Transfer). Other patterns exist — GraphQL (one endpoint, query language), tRPC (typed RPC for full-stack TypeScript apps), gRPC (protobuf, server-to-server). REST is the dominant one for public APIs.

Section · 07

Promise.all — run things in parallel

// Wrong — sequential, slow
const user = await loadUser(123);
const orders = await loadOrders(123);
const reviews = await loadReviews(123);

// Right — parallel, fast
const [user, orders, reviews] = await Promise.all([
  loadUser(123),
  loadOrders(123),
  loadReviews(123),
]);

Promise.all fires every promise immediately and waits for all of them. If any one rejects, the whole thing rejects (use Promise.allSettled if you want to wait for all regardless).

You now know how to talk to a server. The next lesson covers what’s actually on the other side — server-side code, where it runs, and why secrets never live in browser JavaScript.