fetch & JSON
Request JSON with fetch, inspect Response status, parse response bodies safely, and tell HTTP errors apart from network failures.
- 01Read an HTTP responseName the parts of a request and a response: method, URL, headers, body, status, and
response.ok. - 02Read JSON safelyCall
response.json()only when there is a JSON body to read. - 03Handle the right failureTell a bad HTTP status apart from a
fetchthat rejects.
Requests, replies, and fetch
Web pages often need data from a server: a user's profile, a list of products, or today's weather. The page asks for the data, and the server sends it back. fetch is the browser function that does the asking.
You give fetch an address, called a URL, such as /api/users/42. You can also give it some settings. fetch sends a request to that address. The server sends back a response.
The trip over the network takes time, so fetch can't give you the response straight away. It gives you a Promise: an object that will hold the result later. You wait for it with await. When it is ready, you get a Response object.
The most common job is to ask a server for JSON. JSON is data written as text, like {"name":"Ada"}. The job has three steps:
- Wait for the
Responsewithawait fetch(url). - Check whether the server says the request worked.
- Wait again, with
await response.json(), to read the JSON and turn it into a JavaScript object.
You send a letter by post. The address on the front says where it goes. Notes on the envelope tell the reader how to handle it. The letter inside is what you want to say. The post office gives you a tracking slip. Later a reply arrives, with a stamp that says how things went. You still have to open it to read it.
- In real life: The letter you send
- In JavaScript: The request: the method, the URL, the headers, and the body
- In real life: The tracking slip from the post office
- In JavaScript: The Promise that
fetchreturns - In real life: The reply in its envelope
- In JavaScript: The
Responseobject - In real life: Opening the envelope to read the reply
- In JavaScript: A body method, such as
response.json()
A Promise resolves when it finishes with a result. It rejects when it finishes with an error. Here is the most important fact in this lesson: the Promise from fetch resolves whenever the server replies. It resolves even when the reply says "404 Not Found". It rejects only when no reply reaches your code, for example when you are offline.
How HTTP works
MAPHTTP is the set of rules that browsers and servers use to talk. Web pages, JSON data, and images all travel with HTTP. The main rule is simple: the browser sends one request, and the server sends back one response.
Every request has a method. The method says what you want. GET means "give me this". POST means "here is something new to save".
A request and a response have the same three parts. Read the table one row at a time: each row is one part, first in the request, then in the response.
| Part | In the request | In the response |
|---|---|---|
| First line | The method and the address. GET /api/users/42 means "give me user 42". | The status code. 200 OK means it worked. 404 Not Found means nothing is at that address. |
| Headers | Extra notes about the request, such as Content-Type, which says what kind of data the body is. | Extra notes about the response, such as Content-Type: application/json. |
| Body | The data you send, if any. GET and HEAD requests can't have a body. | The data that comes back: JSON, HTML, plain text, a file, or nothing at all. |
The status code is a three-digit number. Its first digit tells you its group. The table below lists each group, what it means, and codes you will see often.
| Group | What it means | Codes you will see |
|---|---|---|
| 1xx | Information: the request is still going. | You rarely see these when you use fetch. |
| 2xx | Success: it worked. | 200 OK, 201 Created, 204 No Content |
| 3xx | Look somewhere else. A redirect says "try this other address". 304 says "your saved copy is still good". | 301, 302, 304. fetch follows redirects for you. |
| 4xx | A problem with the request, on your side. | 400, 401, 403, 404, 409, 422, 429 |
| 5xx | A problem on the server. | 500, 502, 503 |
In JavaScript, a Response object shows these parts as properties:
status: the status code, such as404.statusText: the words after the code, such asNot Found. Newer versions of HTTP often leave it empty, so don't rely on it.ok:truewhen the status is from 200 to 299.headers: the extra notes.url: the final address the response came from.redirected:truewhenfetchfollowed a redirect to get here.type: a word that says how the response was fetched, such as"basic"for your own site.
You can make a Response yourself with new Response(body, options). That is handy for practice, because you don't need a server. The code below makes one with status 201 and one header.
const response = new Response("hello", {
status: 201,
statusText: "Created",
headers: { "X-Request-Id": "abc-123" },
});
console.log(response.status);
console.log(response.headers.get("x-request-id"));
console.log(response.headers.get("content-type"));The three lines print 201, abc-123, and text/plain;charset=UTF-8. Line 9 asks for content-type, which the code never set. The body is a string, so Response adds a plain-text type by itself. A header that is really missing gives null.
fetch basics
CODEThe shortest form is await fetch(url). With no settings, fetch:
- uses the
GETmethod, - follows redirects for you,
- sends cookies only to your own site. A cookie is a small piece of data the browser keeps for a site, such as who is logged in. This setting is called
credentials: "same-origin".
"Your own site" means the same origin: the same scheme, domain, and port as the page, such as https://example.com. The next lessons cover request settings, caching, and CORS.
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; }}fakeFetch stands in for fetch on this page; the note below explains why. HttpError is an error class this lesson made: an Error that also holds the status. Read the function in this order:
- Line 3 waits for the
Response. - Line 6 checks for status 204, which means "no content". There is no body to read, so the function stops there.
- Lines 11 to 14 check the
Content-Typeheader. If the server didn't send JSON, the function throws an error. - Line 16 waits a second time, to read the body as JSON.
- Lines 17 to 19 handle a bad status. The function throws an
HttpError, so the code that called it knows something went wrong. - Line 21 catches any error, prints its name, and throws it again for the caller.
This course runs as a plain website, and it must not call other people's servers. So the experiments use fakeFetch: a tiny pretend server inside the page. It waits a moment, then returns a real Response object. The page also runs a few real fetch calls that are safe: to this site, and to data: addresses that hold their data inside the address itself.
Headers are like a list of named notes. Read one with response.headers.get(name). You get the value as a string, or null if that header is missing. Header names ignore upper and lower case.
const response = new Response("tea", {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
console.log(response.headers.get("content-type"));Line 2 writes the name as Content-Type, with capitals. Line 5 asks for content-type, all lower case, and still finds it. It prints text/plain; charset=utf-8.
Search boxes and filters send the user's words in the URL, after a ?. This part of the URL is called the query string. Don't glue it together yourself. Spaces and symbols such as & have special meanings in a URL. URLSearchParams writes them safely for you.
const params = new URLSearchParams({
q: "green tea",
page: "2",
});
console.log("/api/search?" + params);Lines 1 to 4 make two named values, q and page. Line 6 turns them into text and adds it after ?. It prints /api/search?q=green+tea&page=2. The space became a +.
const params = new URLSearchParams({ q: "tea & cake" });
const url = new URL("https://example.test/api/search");
url.search = params;
console.log(url.href);Line 2 makes a URL object from a full address. Line 3 puts the query string into its search part. Line 5 prints https://example.test/api/search?q=tea+%26+cake. The & in tea & cake became %26, so it stays part of the search words. A plain & would start a new value.
response.ok and status
INTERACTIVEresponse.ok is true or false. It is true when the status is from 200 to 299, and false for every other status. That is all it checks. It doesn't check that the body is valid JSON. It doesn't check that the data is what your app wanted.
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
Pick a server answer and press Run. The same careful function handles every answer.
Every answer is a real Response object. 404 and 500 still resolve. Only the network error rejects, because no Response exists.
Pick 404, then 500, and press Run. The fetch Promise still resolves, because the server replied. Your code decides what happens next. A common choice is to read the error message from the JSON body, then throw a custom error that holds the status.
response.json()
STEP THROUGHWhen fetch resolves, the body may still be arriving. So response.json() returns another Promise, and you await it a second time. It reads the whole body and turns the JSON text into a JavaScript object.
await gives you the Response, with its status. The second await reads the body and gives you the data.response.json() can reject even when fetch worked:
- The body is not valid JSON. It rejects with
SyntaxError. - There is no body, as with status 204. There is nothing to read, so it also rejects with
SyntaxError. - The body was already read. It rejects with
TypeError.
A letter arrives. Holding the envelope is not the same as reading the letter. You still have to open it. When you do, the handwriting might be unreadable. Or the envelope might be empty.
- In real life: The envelope arrives
- In JavaScript:
fetchresolves with aResponse - In real life: You open it and read the pages
- In JavaScript:
response.json()reads the body - In real life: The handwriting can't be read
- In JavaScript: Invalid JSON rejects with
SyntaxError - In real life: The envelope is empty
- In JavaScript: A 204 response has no body to read
Step through loadUser below. It asks the pretend server for user 42 and returns the user's name. Try each server answer:
- With 200, the function reaches line 9 and returns
"Ada". - With 404,
fetchresolves, but the function stops at line 5 and throws. - With a network error,
fetchrejects, so the function stops at line 2.
Pick a server answer, then step through to see where the 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;}A body can be read only once. The first read uses it up, and response.bodyUsed becomes true. A second read rejects with TypeError.
json() reads the body and uses it up. The second json() has nothing left to read, so it rejects.Try both buttons in the demo: read twice fails, and clone first works.
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 can be read once. After json(), text(), blob(), arrayBuffer(), or formData() reads it, bodyUsed becomes true. If two parts of your code need the body, clone the response before the first read.
Every body method follows the same one-read rule. Each one reads the body in a different form:
json()reads JSON and gives an object.text()reads plain text or HTML and gives a string.blob()reads a file, such as an image.arrayBuffer()reads raw bytes.formData()reads form fields.
If two parts of your code both need the body, call response.clone() before either one reads it. The copy gets its own body.
Pick the method that matches the data. For plain text, use text(). Calling json() on plain text would reject, because the text is not JSON.
const response = new Response("plain text");
console.log(await response.text());The body is the text plain text, so line 3 prints exactly that. After this read, the body is used up. Calling json() on the same response now would reject.
const response = new Response("{ bad", {
headers: { "Content-Type": "application/json" },
});
response.json().catch((error) => console.log(error.name));The Response is fine, but its body, { bad, is not valid JSON. .catch() runs a function when a Promise rejects. Here, line 5 prints SyntaxError. This is a different failure from a network error, where fetch itself rejects.
Real requests in your browser
REAL FETCHThe pretend server gives the same result every time, which is good for learning. But fetch is a real browser tool, so let's run it for real. These four requests are safe, and none of them contacts another site:
- this page,
- a page on this site that doesn't exist,
- a
data:URL, which holds its JSON inside the address itself, so no server is needed, - a cancelled blob URL. A blob URL is an address for data kept in your browser's memory. Once it is cancelled, it points at nothing.
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 that are safe on this site: this page, a missing page on this site, a data URL, and a cancelled blob URL. None of them contacts another site.
What each button shows:
- fetch this page: a response can hold HTML, not JSON.
- missing page (404): a missing page gives a response with
ok: false. - data URL JSON:
json()can read JSON with no server at all. - cancelled blob URL: when nothing can answer,
fetchrejects withTypeError.
Network errors vs HTTP errors
SORTMany people think every bad status makes fetch reject. It doesn't. A response with 404, 429, or 500 is still a response. These are HTTP errors: the server replied, and the reply says something went wrong.
fetch rejects only when there is no response your code can use. These are network errors:
- You are offline, or the server can't be found.
- The URL is not valid.
- The browser blocks the response because of CORS, a safety rule explained below.
- The request is cancelled, which is also called aborted.
fetchthen rejects with the reason given for the cancel. Often that is anAbortError.
fetch resolves and ok is false. Only when no reply arrives does fetch reject.- A 404 response with a JSON error message
- A 500 server error response
- No internet, or the server can't be found
- The browser blocks it because of CORS
- An AbortController cancels it
- A 200 response whose body is
{ nope
Put each case in its group: fetch resolves, fetch rejects, or json() rejects.
CORS is a browser safety rule. It decides whether a page may read responses from another site. You'll learn it two lessons from now. For now, remember one thing: when the browser blocks a response from another site, your code can't see its status or body. fetch rejects with TypeError.
A timeout also makes fetch reject. A timeout is a time limit. AbortSignal.timeout(1000) makes a signal: an object that cancels a request when it fires. This one fires by itself after 1000 milliseconds, which is 1 second. So the request can't wait forever.
const signal = AbortSignal.timeout(1000);
console.log(signal.aborted);
console.log(signal instanceof AbortSignal);The signal has not fired yet, so line 3 prints false. Line 4 prints true, because it is an AbortSignal. You can pass it to fetch as the signal setting. The cancellation lesson shows how to combine signals and handle the timeout error.
const signal = AbortSignal.timeout(1000);
const options = { signal };
console.log(options.signal === signal);
console.log(signal.aborted);Line 2 puts the signal into a settings object. Making this object doesn't send anything. It only holds the signal, ready for fetch(url, options). Line 4 prints true, because it is the same signal. Line 5 prints false, because a second hasn't passed yet.
Where you will use this
PRACTICALPages fetch JSON whenever they need fresh data. For example, when they:
- load a user's profile,
- save a to-do item,
- search while the user types,
- refresh a dashboard,
- send a contact form.
Pages read JSON often, and they send it often too.
A small helper function keeps the status check in one place. This one takes the fetch function as a parameter. In a test, you can pass a stand-in that returns a Response you made yourself. Then the test gets the same result every time, with no live server.
async function getJSON(url, fetcher = fetch) {
const response = await fetcher(url);
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
return response.json();
}
const fetcher = () => Promise.resolve(new Response(
JSON.stringify({ name: "Asha" }),
{ headers: { "Content-Type": "application/json" } },
));
console.log((await getJSON("/api/user", fetcher)).name);Line 2 waits for the response. Lines 3 to 5 throw an error if the status is not OK. Only then does line 6 read the JSON. Lines 9 to 12 make a stand-in fetcher that returns a real Response holding {"name":"Asha"}. So line 14 prints Asha.
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. To send JSON:
- Use a method that allows a body, such as
POST,PUT, orPATCH(line 4).GETrequests can't have a body. - Set the
Content-Typeheader toapplication/json(line 5). It tells the server how to read the body. - Turn your object into JSON text with
JSON.stringify(todo)(line 6).
const todo = { title: "Buy tea", done: false };
const request = new Request("/api/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(todo),
});
console.log(request.method);
console.log(request.headers.get("content-type"));
console.log(await request.text());new Request() takes the same address and settings as fetch, but it only builds the request. It doesn't send it. Lines 8 to 10 print POST, application/json, and {"title":"Buy tea","done":false}. The body really is text.
Real apps often put all of this into one helper function. It does these steps in order:
- Wait for
fetch, and handle a network error. - Check
response.ok. - Skip reading when the status is 204.
- Read JSON only when the response should have it.
- For a bad status, throw a custom error that holds the status and the data.
Common misconceptions
WATCH OUT- "fetch rejects on 404." No. It resolves with a response whose
okisfalse. - "response.json is the JSON." No. It is a method. Calling it returns a Promise for the data.
- "ok means my app is happy." No. It only means the status is from 200 to 299.
- "Every response has JSON." No. A response can hold HTML, a file, invalid text, or nothing at all, as with 204.
- "I can read the body again and again." No. A body can be read once. Call
clone()first if you need two reads. - "Header names care about upper and lower case." No.
headers.get("content-type")findsContent-Type.
The table compares three Promises that people often mix up. Read across a row to see when that Promise resolves, and why it usually rejects.
| Which Promise | When does it resolve? | Why does it usually reject? |
|---|---|---|
fetch(url) | When the status and headers of the response arrive. | A network problem, a CORS block, a bad URL, or a cancel. |
response.json() | When the whole body is read and turned into an object. | The JSON is invalid, the body is empty, or the body was already read. |
Your own helper, such as getJSON | When it returns the data. | Any error you choose to throw, such as an HttpError. |
Practice exercises
5 EXERCISESWhat two values does this code print?
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 a real response, but its status is not between 200 and 299. So the two lines print false and 404.
Your helper crashes when the server replies 204 No Content. Which Response property should you check before you read the body?
Check response.status. If it is 204, return before reading the body. A 204 response has no body on purpose.
What error name does the .catch() print?
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 tries to read it as JSON. not json is not valid JSON, so it rejects with SyntaxError, and the catch prints that name.
This code copies the response, then reads both. What two values print?
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);response.clone() makes a copy with its own body. The original and the copy are each read once, so both json() calls work and both lines print true.
Which header tells the server that 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),
});Set Content-Type: application/json, send a body made with JSON.stringify, and use a method such as POST, not GET.
Check your understanding
10 QUESTIONSQuestion 1 of 10When does the Promise from fetch resolve?
Choose an answer to see the explanation.
Question 2 of 10What 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 10The server replies with status 204. What should your code do before it calls response.json()?
Choose an answer to see the explanation.
Question 4 of 10The body is not valid JSON. What does this print?
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 10Which request sends JSON correctly?
Choose an answer to see the explanation.
Question 6 of 10What happens if you call response.json() twice on the same response?
Choose an answer to see the explanation.
Question 7 of 10Which is true for browser fetch when you pass no settings?
Choose an answer to see the explanation.
Question 8 of 10How should you add a search term to a fetch URL?
Choose an answer to see the explanation.
Question 9 of 10What does AbortSignal.timeout(1000) give you?
Choose an answer to see the explanation.
Question 10 of 10What should a getJSON helper do before it calls response.json()?
Choose an answer to see the explanation.
Key takeaways
fetch(url, options)returns a Promise. It resolves with aResponsewhen the status and headers arrive.- A bad status such as 404 or 500 doesn't make
fetchreject. Checkresponse.okandresponse.statusyourself. response.json()is a second wait. It reads the body once, and it can reject.fetchrejects only when no usable response arrives: a network problem, a CORS block, a bad URL, or a cancel.- To send JSON, use a method such as
POST, setContent-Type: application/json, and sendJSON.stringify(data).
Final definition: fetch sends an HTTP request and resolves with a Response. Your code checks the status, then reads the body once with the right method, such as json().
Up next: Requests in depth. You'll learn more about methods, headers, bodies, and the Request and Response objects.