fetch & JSON
Request JSON with fetch, inspect Response status, parse response bodies safely, and tell HTTP errors apart from network failures.
- 01Read an HTTP responseConnect method, URL, headers, body, status, and response.ok.
- 02Parse JSON safelyUse response.json only when there is a JSON body to read.
- 03Handle the right failureSeparate HTTP error statuses from rejected fetch promises.
Requests, replies, and fetch
fetch is the browser’s promise-based way to make network requests. You give it a resource, usually a URL, and optional settings. It gives you a Promise for a Response object once the reply’s headers arrive.
The most common beginner task is: ask for JSON, check whether the server says it worked, then parse the JSON body. That sentence hides three separate waits: the request travels, the response headers arrive, and the body streams in.
Think of HTTP as ordering by mail. The request is your letter: the method says what you want done, the URL is the address, headers are notes on the envelope, and the body is the contents. The response is the reply with a status code stamped on it.
- In real life: The letter you send
- In JavaScript: The request: method, URL, headers, and body
- In real life: The tracking slip
- In JavaScript: The Promise returned by
fetch - In real life: The reply envelope
- In JavaScript: The
Responseobject - In real life: Opening the envelope
- In JavaScript: A body method such as
response.json()
Where the analogy stops: Mail does not have browser security rules. Real fetch also deals with redirects, credentials, CORS, abort signals, streaming bodies, and caches.
That tracking slip is important. It resolves when the reply envelope arrives, even if the stamp says “404 not found.” It rejects only when the letter never gets there or the browser refuses to hand you the response.
HTTP in a nutshell
MAPHTTP is the request/response protocol browsers use for web pages, JSON APIs, images, and many other resources. A client sends one request. A server sends one response. fetch is JavaScript’s convenient handle for that exchange.
| Piece | Request | Response |
|---|---|---|
| Start line | GET /api/users/42 asks for an address with a method. | HTTP 200 OK or 404 Not Found reports the result. |
| Headers | Extra notes such as Content-Type or Accept. | Extra notes such as Content-Type: application/json. |
| Body | Optional contents. GET and HEAD requests cannot have a body. | Optional contents: JSON, HTML, text, a file, or nothing. |
| Family | Meaning | Common examples |
|---|---|---|
| 1xx | Informational | Rare in everyday fetch code; the request is continuing. |
| 2xx | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirects and caching | 301, 302, 304. fetch follows redirects by default. |
| 4xx | Client-side problem | 400, 401, 403, 404, 409, 422, 429 |
| 5xx | Server-side problem | 500, 502, 503 |
The Response object exposes that stamp through status, statusText, ok, headers, url, redirected, and type. statusText can be empty on modern HTTP versions, so do not build logic on the words.
fetch basics
CODEThe simplest shape is await fetch(url). It uses GET, follows redirects, and uses credentials: "same-origin" by default, which means same-origin cookies can go with same-origin requests. Request options, cache modes, and CORS get their own next lessons.
async function carefulFetch(url) { try { const response = await fakeFetch(url); console.log("resolved", response.ok, response.status); if (response.status === 204) { console.log("no body to parse"); return null; } const type = response.headers.get("content-type") || ""; if (!type.includes("application/json")) { throw new Error("Expected JSON"); } const data = await response.json(); if (!response.ok) { throw new HttpError(response.status, data.message); } return data; } catch (error) { console.log("caught", error.name); throw error; }}Notice the order. First await fakeFetch gets a Response. Then the code checks special statuses and headers. Then await response.json() reads the body. Finally, the code decides whether a non-OK HTTP status should become a thrown error for the caller.
This course is statically exported and must not call external APIs. The playgrounds use a tiny in-page fake server that returns real Response objects, plus safe same-origin and data: requests your browser can perform.
response.ok and status
INTERACTIVEresponse.ok is a convenience boolean: true for 200 through 299, false for everything else. It does not mean the JSON body is valid. It does not mean your app likes the data. It only summarizes the HTTP status range.
async function carefulFetch(url) { try { const response = await fakeFetch(url); console.log("resolved", response.ok, response.status); if (response.status === 204) { console.log("no body to parse"); return null; } const type = response.headers.get("content-type") || ""; if (!type.includes("application/json")) { throw new Error("Expected JSON"); } const data = await response.json(); if (!response.ok) { throw new HttpError(response.status, data.message); } return data; } catch (error) { console.log("caught", error.name); throw error; }}- 1
Choose a response and press Run. The same careful function handles every case.
Every option uses real Response objects. Notice that 404 and 500 resolve, while the network option rejects before a Response exists.
Try 404 and 500. The fetch promise resolves because the server replied. Your code chooses what to do next. A common pattern is to parse an error JSON body, then throw a custom custom error containing the status.
response.json()
STEP THROUGHresponse.json() opens the envelope and reads the contents. It returns another Promise because the body may still be streaming. If the body is not valid JSON, that Promise rejects with SyntaxError. If the response has no body, such as 204, parsing also fails.
The Response is only the envelope. Calling json() opens it and reads the letter. That can fail separately from delivery: maybe the server sent HTML, an empty body, or broken JSON text.
- In real life: Envelope arrives
- In JavaScript: fetch resolves to
Response - In real life: Open and read the pages
- In JavaScript:
response.json()reads the body stream - In real life: Unreadable handwriting
- In JavaScript: Invalid JSON rejects with
SyntaxError - In real life: Empty envelope
- In JavaScript: 204 has no body to parse
Where the analogy stops: An envelope can be opened and reread on paper. A Response body is a stream, so the body methods consume it once.
Choose a server answer, then step through where the async function stops.
script
const response = await fakeFetch("/api/users/" + id); if (!response.ok) { throw new HttpError(response.status, "User not found"); } const user = await response.json(); return user.name;}const response = new Response(JSON.stringify({ ok: true }), { headers: { "Content-Type": "application/json" },}); const first = await response.json();console.log(first.ok); const second = await response.json();console.log(second.ok);- 1
Run the demo to see bodyUsed change.
A Response body is a stream. Once json(), text(), blob(), arrayBuffer(), or formData() consumes it, bodyUsed becomes true. Clone before reading if two consumers need it.
Other body methods follow the same one-use rule: text(), blob(), arrayBuffer(), and formData(). If two pieces of code need the body, call response.clone() before either reads it.
Real requests in the reader’s browser
REAL FETCHFake servers are useful for repeatable lessons, but fetch is a real browser API. This experiment runs only safe requests the static site can serve: this page, a missing same-origin page, a data: URL containing JSON, and a revoked blob URL that rejects without leaving your machine.
await fetch(location.href);await fetch("/javascript/this-page-does-not-exist");await fetch('data:application/json,{"name":"Ada"}');const url = URL.createObjectURL(new Blob(["gone"]));URL.revokeObjectURL(url);await fetch(url);- 1
Press a button to run a real browser fetch.
These are real fetch calls the static site can serve safely: this page, a same-origin 404, a data URL, and a revoked blob URL that rejects without leaving your machine.
The page request proves a response can be HTML instead of JSON. The missing path proves a same-origin 404 has ok: false. The data: request proves json() can parse JSON without an API server. The revoked blob URL proves a real network-style failure rejects with TypeError in the browser.
Network errors vs HTTP errors
SORTThe biggest fetch misconception is thinking every bad status rejects. It does not. A response with 404, 429, or 500 is still a response. Rejections are for “no usable response,” such as a network failure, invalid URL, browser CORS block, or abort. Aborts reject with the signal’s reason; many browser examples show an AbortError.
- 404 JSON error response
- 500 server error response
- DNS failure or offline
- CORS blocked by the browser
- AbortController cancels it
- 200 response containing
{ nope
Sort each outcome by which promise rejects.
CORS is the next-but-one lesson. For now, remember the behavior: when the browser blocks a cross-origin response, your code does not get to inspect the status or body. fetch rejects with a TypeError.
Where you will use this
PRACTICALFetching JSON appears anywhere a page needs fresh data: loading a profile, saving a todo, searching as the user types, refreshing a dashboard, or submitting a contact form. Reading JSON is common, but sending JSON is just as important.
const todo = { title: "Learn fetch", done: false }; await fetch("/api/todos", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(todo),});A JSON request body is text. Use JSON.stringify(data) and set Content-Type: application/json so the server knows how to read it. Use POST, PUT, or PATCH for a body; GET requests cannot have one.
Real apps often wrap fetch in a helper: await fetch, handle network rejection, check response.ok, skip 204, parse JSON only when expected, and throw a custom error with status and data.
Common misconceptions
WATCH OUT- “fetch rejects on 404.” It resolves with
ok: false. - “response.json is the JSON.” It is a method that returns a Promise for parsed data.
- “ok means my app is happy.” It only means the status is 200–299.
- “Every response has JSON.” HTML pages, empty 204 replies, files, and invalid bodies exist.
- “I can read the body over and over.” Body methods consume the stream once; use
clone(). - “Headers are case-sensitive.”
Headers.getis case-insensitive, socontent-typeworks.
| Thing | Resolves when | Common rejection |
|---|---|---|
fetch(url) | Response headers arrive | Network/CORS/invalid URL/abort |
response.json() | The body is read and parsed | Invalid JSON, empty body, or body already used |
| Your helper | You return parsed data | Whatever errors you choose to throw, such as HttpError |
Practice exercises
5 EXERCISESType the two values printed by this Response.
const response = new Response("{}", { status: 404 });
console.log(response.ok);
console.log(response.status);const response = new Response("{}", { status: 404 });
console.log(response.ok);
console.log(response.status);A 404 Response is real, but not ok. The two logs are false and 404.
Your helper crashes on 204 No Content. Which Response property should you check before parsing?
Check response.status for 204 and return before parsing. A 204 body is intentionally empty.
Predict the error name logged by the catch handler.
const response = new Response("not json", {
headers: { "Content-Type": "application/json" },
});
response.json().catch((error) => console.log(error.name));const response = new Response("not json", {
headers: { "Content-Type": "application/json" },
});
response.json().catch((error) => console.log(error.name));response.json reads the body and JSON.parse rules reject the text, so the logged error name is SyntaxError.
What two values print after cloning the response?
const response = new Response(JSON.stringify({ ok: true }));
const copy = response.clone();
console.log((await response.json()).ok);
console.log((await copy.json()).ok);const response = new Response(JSON.stringify({ ok: true }));
const copy = response.clone();
console.log((await response.json()).ok);
console.log((await copy.json()).ok);The original and the clone each have a stream, so both json calls resolve and both logs are true.
Which header tells the server the request body is JSON?
const todo = { title: "Learn fetch", done: false };
await fetch("/api/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(todo),
});const todo = { title: "Learn fetch", done: false };
await fetch("/api/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(todo),
});Use Content-Type: application/json with a stringified body on a non-GET request.
Check your understanding
7 QUESTIONSQuestion 1 of 7When does the promise returned by fetch resolve?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictconst response = new Response("{}", { status: 404 }); console.log(response.ok);Choose an answer to see the explanation.
Question 3 of 7What should you do before parsing a 204 response as JSON?
Choose an answer to see the explanation.
Question 4 of 7What does invalid JSON do?
Read the code, then predictconst response = new Response("not json"); response.json().catch((error) => console.log(error.name));Choose an answer to see the explanation.
Question 5 of 7Which request sends JSON correctly?
Choose an answer to see the explanation.
Question 6 of 7What happens if you call response.json twice on the same response?
Choose an answer to see the explanation.
Question 7 of 7Which default is true for browser fetch?
Choose an answer to see the explanation.
Key takeaways
fetch(resource, init)returns a Promise for aResponseonce headers arrive.- HTTP error statuses do not reject fetch. Check
response.okandresponse.status. response.json()is a second await that consumes the body once and can reject.- Network failures, CORS failures, invalid URLs, and aborts reject the fetch promise before you can inspect a Response.
- Send JSON with a non-GET method,
Content-Type: application/json, andJSON.stringify(data).
Final definition: fetch sends an HTTP request and resolves to a Response envelope; your code inspects the status, then reads the body with the right one-use body method.
Up next: Requests in depth — methods, headers, bodies, and the Request and Response objects.