Command Palette

Search for a command to run...

Hectal
PHASE 11Intermediate ~25 min· topic 2 of 8

Topic 11.2

fetch

In one line

fetch(url, options) sends an HTTP request and returns a promise for a Response; it only rejects on network failure, so you must check response.ok yourself, read the body once with json() or text(), and use an AbortController to cancel or time out.

Think of it like this

Sending a letter and getting a reply. You post a letter (the request) and get a receipt straight away: a promise that a reply will come. When the reply arrives, you first look at the envelope (status and headers) and only then open it and read the letter inside (the body). An envelope stamped "address unknown" is still a reply; only a lost letter means the post itself failed. And if you change your mind, you can phone the post office and cancel the delivery.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

fetch
The browser function that sends an HTTP request and returns a promise for the response.
Response
The object fetch gives you: status, headers and a body you can read once.
response.ok
true when the status is 200–299, false for anything else.
Body
The data part of a response or request, like the JSON text. Reading it takes time, so it returns a promise.
Network error
No usable reply at all (offline, wrong address, blocked by CORS). Only this makes fetch reject.
AbortController
An object with a signal you hand to fetch and an abort() method that cancels the request.
AbortError
The error name a fetch rejects with when you cancel it.
TimeoutError
The error name when AbortSignal.timeout(ms) cancels a request because it took too long.
data: URL
A URL that contains its own content, like data:application/json,{"a":1}. Fetching it needs no network, so Hectal's examples use it.

Step by step

01The two awaits

First wait for the response (status and headers), then wait for the body. Forgetting the second await gives you a pending promise instead of data.

get.jswhole filejavascript
const res = await fetch("https://api.example.com/users/42");
console.log(res.status);      // 200
const user = await res.json(); // second wait: download and parse the body
console.log(user.name);        // "Asha"

02Check ok, every time

Turn HTTP errors into real errors so they end up in your catch, just like network errors. Include the status in the message so logs are useful.

get-json.jswhole filejavascript
async function getJSON(url, options) {
  const res = await fetch(url, options);
  if (!res.ok) {
    throw new Error("HTTP " + res.status + " for " + url);
  }
  return res.json();
}

03Send JSON with POST

Three things: the method, a Content-Type header that says the body is JSON, and the body turned into a string. A 201 Created reply often includes the saved item with its new id.

post.jswhole filejavascript
const res = await fetch("https://api.example.com/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Asha", city: "Pune" }),
});
if (!res.ok) throw new Error("HTTP " + res.status);
const created = await res.json(); // { id: 7, name: "Asha", city: "Pune" }

04Which outcome happened?

Every request ends in one of four places. Your code decides what the user sees in each one.

Which outcome happened?diagram
Rendering diagram…

05Cancel and time out

Pass a signal. AbortSignal.timeout(ms) cancels by itself; an AbortController lets you cancel, for example when the user types a new search before the old one finished. To have both, combine them with AbortSignal.any([controller.signal, AbortSignal.timeout(8000)]).

timeout.jswhole filejavascript
try {
  const res = await fetch("/api/report", { signal: AbortSignal.timeout(5000) });
  // ...
} catch (err) {
  if (err.name === "TimeoutError") showMessage("The server is slow. Try again.");
  else if (err.name === "AbortError") { /* cancelled on purpose */ }
  else throw err;
}

06Cancel stale searches

A search box that fetches on every keystroke can show the wrong results: an old, slow reply arrives after a newer, fast one. Aborting the previous request before starting the next one fixes that race. Combine it with debouncing (Topic 9.7) to send fewer requests in the first place.

search.jswhole filejavascript
let current;
async function search(term) {
  current?.abort();               // cancel the previous request
  current = new AbortController();
  try {
    const res = await fetch("/api/search?q=" + encodeURIComponent(term), { signal: current.signal });
    render(await res.json());
  } catch (err) {
    if (err.name !== "AbortError") throw err;
  }
}

Try it yourself

  1. 1

    Change the data URL

    In the first example, add "age":30 to the JSON in the URL and print the age on the page. Then break the JSON on purpose (remove the closing }) and predict which line throws: the fetch or the json()?

  2. 2

    Read the body twice

    In the first example, add await res.text(); after res.json(). Predict the error before running. Then fix it with res.clone().

  3. 3

    Move the timeout

    In the third example, change the last timeout from 300 to 1000. Predict the output. Remember to keep the total under the example's 1.2 second wait.

Code & diagrams

fetch a data: URL (no network needed) ES2017+ New tab

A `data:` URL carries its own content, so this is a real `fetch` with a real `Response`, but no server. After `json()`, `bodyUsed` is `true`: the body can't be read again.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

true 200 application/json
{ name: 'Asha', city: 'Pune' }
body used: true
getJSON: POST, check ok, handle every outcome ES2018+ New tab

The 422 reply resolved normally; `postJSON` turned it into an error because `res.ok` was false. Only the network failure was a rejected promise from fetch itself. Clear the input and press Save to see the error on the page.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

saved { id: 7, name: 'Asha' }
server said no: HTTP 422: name is required
network problem: Failed to fetch
Cancel with AbortController, time out with AbortSignal.timeout New tab

`abort()` with no argument rejects with an `AbortError`; `AbortSignal.timeout()` rejects with a `TimeoutError`, so your code can tell "the user cancelled" from "the server was too slow". The messages shown are Chrome's.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

real fetch, aborted -> AbortError: signal is aborted without reason
100ms with 400ms timeout -> done after 100ms
900ms with 300ms timeout -> TimeoutError: signal timed out
A production-ready helper (real network, not live)javascript

Checks the content type before parsing, handles 204, adds a timeout and still accepts the caller's own signal.

export async function request(url, { method = "GET", body, timeout = 8000, signal } = {}) {
  const signals = [AbortSignal.timeout(timeout)];
  if (signal) signals.push(signal);
  const res = await fetch(url, {
    method,
    headers: body ? { "Content-Type": "application/json", Accept: "application/json" } : { Accept: "application/json" },
    body: body ? JSON.stringify(body) : undefined,
    signal: AbortSignal.any(signals),
  });
  const isJSON = res.headers.get("content-type")?.includes("application/json");
  const data = res.status === 204 ? null : isJSON ? await res.json() : await res.text();
  if (!res.ok) {
    const err = new Error(`HTTP ${res.status} ${method} ${url}`);
    err.status = res.status;
    err.data = data;
    throw err;
  }
  return data;
}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget to check res.ok

In the second example, delete the if (!res.ok) { ... } block from postJSON and run.

terminal
$ Console
── what you'll see ──
saved { id: 7, name: 'Asha' }
saved { error: 'name is required' }
network problem: Failed to fetch

Break #2

Send an object as the body

Write body: { name: "Asha" } instead of body: JSON.stringify({ name: "Asha" }) in a real fetch.

terminal
$ Network tab → Payload
── what you'll see ──
[object Object]

Break #3

Give a GET request a body

Call fetch("data:,x", { method: "GET", body: "x" }).

terminal
$ Console
── what you'll see ──
Uncaught (in promise) TypeError: Failed to execute 'fetch' on 'Window': Request with GET/HEAD method cannot have body.

Myth vs fact

Myth

fetch rejects when the server returns 404 or 500.

Fact

It resolves for every HTTP status. It only rejects for network failures, CORS blocks, invalid URLs and aborts. Check res.ok.

Myth

fetch has a default timeout.

Fact

It doesn't. The browser may give up after a long time on a dead connection, but a slow server can keep you waiting for minutes. Add AbortSignal.timeout().

Myth

Aborting a fetch stops the server from doing the work.

Fact

Aborting closes the connection and frees the browser side. A server that already received a POST may still finish it. Design write endpoints to be safe against that (for example with idempotency keys).

Myth

You need a library like Axios to do HTTP properly.

Fact

fetch covers everything in modern browsers and Node.js 18+. Libraries add conveniences (automatic JSON, interceptors, throwing on 4xx), which a 20-line helper gives you too.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    res.json() waits for the whole body. For large or streaming responses, read res.body (a ReadableStream) with getReader() or for await, and parse as chunks arrive, which is how AI chat UIs show text word by word.

  • ▸

    Upload progress isn't available on fetch in browsers yet; XMLHttpRequest still offers upload.onprogress. Download progress can be computed from res.body chunks and the Content-Length header.

  • ▸

    Browsers limit concurrent connections and prioritise requests. fetch(url, { priority: "high" }) (Fetch Priority, supported in Chromium and Safari) and keepalive: true (lets a small request outlive the page, useful for analytics on pagehide) are worth knowing.

  • ▸

    Error messages differ by browser: Chrome says Failed to fetch, Firefox NetworkError when attempting to fetch resource., Safari Load failed. Test err.name (TypeError, AbortError, TimeoutError), never the message text.

Remember this

  1. 1

    fetch is the browser's built-in function for HTTP requests (it replaced the older XMLHttpRequest). fetch(url) sends a GET and immediately returns a promise (Phase 10). The promise resolves with a Response object as soon as the status line and headers arrive, before the body has finished downloading. So there are always two waits: one for the response, one for the body: const res = await fetch(url); const data = await res.json();.

  2. 2

    The single most important rule: **fetch does not reject on HTTP errors. A 404 or 500 is a successful conversation with a bad answer, so the promise resolves. It only rejects** (with a TypeError, message Failed to fetch in Chrome) when no usable response arrived: you're offline, the domain doesn't exist, the connection dropped, CORS blocked it, or the URL is invalid. That's why every real fetch checks **res.ok**, which is true only for status 200–299, and throws its own error otherwise.

  3. 3

    A Response gives you status, statusText, ok, headers (a Headers object), url (after redirects), redirected and the body. The body is a stream you can read once, with one of: res.json() (parse JSON), res.text(), res.blob() (files and images), res.arrayBuffer() or res.formData(). Each returns a promise. Reading twice throws TypeError: ... body stream already read; res.clone() before the first read if you really need two copies.

  4. 4

    To send data, pass an options object: fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }). The body must be a string, FormData, URLSearchParams, Blob or similar; a plain object is not turned into JSON for you (it becomes the text [object Object]). GET and HEAD requests can't have a body at all. Other useful options: credentials ("same-origin" default, "include" to send cookies cross-origin), cache, mode and signal.

  5. 5

    An **AbortController** cancels a request. Create one, pass controller.signal as the signal option, and call controller.abort(): the fetch promise (or the body read in progress) rejects with a DOMException named **AbortError. For timeouts**, AbortSignal.timeout(5000) gives a signal that aborts by itself after 5 seconds, rejecting with a **TimeoutError**. fetch has no timeout of its own: without one, a hung server can keep your spinner going for minutes.

  6. 6

    Good fetch code handles four outcomes: success (2xx with the body you expected), an HTTP error (!res.ok: show a message based on the status), a network or CORS failure (TypeError), and a cancel or timeout (AbortError/TimeoutError, usually not shown as an error at all because the user moved on). Wrap the pattern in a small helper such as getJSON(url) so every call gets it right.

Explain it without notes

01

Why do you have to check response.ok? When does a fetch promise reject?

02

Why are there two awaits in a typical fetch call?

03

How do you send JSON with fetch, and what goes wrong if you skip JSON.stringify?

04

How do you add a timeout and cancellation to fetch, and how do you tell them apart?

Practice

01

Write async function getName() that fetches data:application/json,{"name":"Meera"}, checks ok, and returns the name. Log the result.

02

Write a fetchWithTimeout(url, ms) helper that rejects with a TimeoutError when the request takes longer than ms.

Trade-offs

  • ↔

    Throwing on every non-2xx response keeps calling code simple, but some statuses (like 404 for "not found yet" or 409 for a conflict) are expected; a helper that returns { ok, status, data } gives callers more control at the cost of more checks.

  • ↔

    Short timeouts make the UI feel responsive and free resources, but on slow mobile networks they can fail requests that would have succeeded; pair them with a retry and a clear message.

Done when you can

  • I can fetch JSON with two awaits and use the data.

  • I always check res.ok before using the body.

  • I can send JSON with POST, the right header and JSON.stringify.

  • I know fetch only rejects on network failures and aborts.

  • I can cancel a request with AbortController and add a timeout with AbortSignal.timeout.

  • I can tell TypeError, AbortError and TimeoutError apart in a catch.