Your first API call
Chapter 01 described what an HTTP request looks like. This chapter sends one. About ten lines of JavaScript are enough to bring real data from a public server into your page. One step in the middle catches almost every beginner, so this chapter covers it early.
fetch and the Promise it returns
fetch does not give you the data. It gives you an object that stands for data which has not arrived yet.
A ticket at the counter
You order a drink at a counter. The staff do not make you stand there and watch. They hand you a numbered ticket, you do something else, and you come back when your number is called.
fetch works the same way. One round trip over the network often takes hundreds of milliseconds, and JavaScript runs on a single thread, so it cannot stop and wait. The moment fetch(url) runs, it returns an object that stands for a result which has not arrived yet and which will later either succeed or fail. That object is called a Promise. When the response arrives, the Promise settles: it either resolves with a value or rejects with an error.
There are two ways to get the value out of a Promise, and they do the same thing. On the left is the older .then chain. On the right is async / await, which is what most code uses now.
Why two awaits?
The two awaits wait for two different things. The first one, await fetch(url), resolves as soon as the response headers have arrived. At that point you can read res.status and res.headers, but the body may still be downloading. The second one, await res.json(), reads the body and parses it into a JavaScript value. Reading the body is a separate asynchronous step, so res.json() returns a Promise of its own.
Three notes. ① await normally has to be inside an async function; ES modules and the browser console also allow it at the top level, which is why you can type it straight into the console for the practice tasks. ② An async function always returns a Promise, whatever value you return inside it. ③ So whoever calls it usually has to await it too. async spreads outward along the call chain. That is expected, not a mistake.
The main trap: fetch does not reject on 404
This is the mistake almost everyone makes on their first API call. It is easier to learn it now than to debug it later.
'Success' means something narrower than you expect
You might expect 404 and 500 to jump into catch. They do not. For fetch, the call succeeded if the request reached the server and a response came back — even when that response says 404. The Promise from fetch rejects only when the request fails at the network level: no connection, a hostname that does not resolve, a request that was aborted, or a response the browser blocked under the CORS rules.
So you have to check the status yourself. res.ok is true when the status code is between 200 and 299. Skip that check and the body of a 404 response gets passed along as if it were normal data. The program then fails much later, far from the real cause, usually with a string of undefined.
catch. The shape try → if (!res.ok) throw → catch is used by every fetch example in this book.res.json() can fail on its own
res.json() reads the response body and parses it as JSON. If the body is not valid JSON, the Promise it returns rejects with a SyntaxError. This happens more often than you would expect. A misconfigured server can answer with an HTML error page and still send status 200, so res.ok is true and the parse fails. An empty body does the same: a 204 No Content response has no body, so calling res.json() on it fails.
Notice that the example above writes return await res.json(), not return res.json(). With the await, the parse happens inside the try block, so this error lands in the same catch as everything else. Without it, the Promise would leave the function before try could see it fail.
From data to page: a Pokemon lookup
JSON in a variable is only half the job. What a user sees is the page, so the last step is putting the data into the DOM.
A small but complete project: type a name, request it from PokeAPI (a free Pokemon database that needs no account), and show the height, the weight, and a picture. Two files.
id, which is how the JavaScript finds it.Loading... so the user knows something is happening. When the data arrives, fields from the JSON are written into the container. When it fails, the message says so. fetch → res.json() → update the DOM is the whole loop. One caution: innerHTML runs whatever HTML it is given, so use textContent for values you only want to show as text.The widget below runs the same logic for real. Try it.
pokeapi.co.Sending data: the three parts of a POST
Every request so far has asked for data. Registering, posting, and ordering send data instead.
id that come back are the standard reply to a successful creation, described in chapter 01.Leave one part out and it breaks
Without JSON.stringify, fetch converts the object to a string the ordinary JavaScript way, and the server receives the text [object Object]. Not one field can be read out of it. Without Content-Type, the server may try to parse your JSON as plain text or as form data; the fields come out empty, or the server answers 400. Without method, the request is a GET, and a GET is not allowed to carry a body, so fetch throws a TypeError before anything is sent. The wording differs between browsers; Chrome says Request with GET/HEAD method cannot have body. That one is easy to notice, because it lands in catch immediately. The first two are much quieter.
When a server says it received nothing, check these three first. Most of the time one of them is missing.
The Network panel in DevTools
When the code does not explain the problem, the messages do. Press F12, open Network, and every request is recorded there. Click through the tabs.
Three groups: General (the full URL, the request method, and the status code), Response Headers (what the server sent back), and Request Headers (what your browser sent). Chapter 01 explains what these headers mean.
Checking whether Content-Type is right, whether a caching header was sent, or whether Authorization was included: this tab is always the first place to look.
A response body can only be read once
res.json() reads the body as a stream: the data passes through once and is then gone. Calling res.json() a second time on the same response throws TypeError: body stream already read. If you need the data twice, store it the first time: const data = await res.json(). If you really need to read the raw body twice, res.clone() gives you a second copy, but you have to call it before either copy is read. DevTools can show you the body again because the browser kept its own copy, not because your code gets a second chance.
A first look at CORS: a browser rule, not a failure
You will meet it sooner or later. It helps to recognize it in advance.
One day you call a new API. curl works. Postman works. But fetch in the browser prints a long red message containing the words CORS policy. The first reaction is usually "the API is down". It is not. The API answered. Your own browser refused to let your script read the answer.
The same-origin policy in one paragraph
Browsers follow a rule called the same-origin policy: a script loaded from one origin may not read a response from a different origin, unless that response says it is allowed. The response says so with a header, Access-Control-Allow-Origin. The set of rules around that header is called CORS (Cross-Origin Resource Sharing).
Two things follow from this. First, the browser enforces the rule, so it protects the user, not the server: curl, Postman, and any program running on a server ignore CORS entirely, and the header keeps nobody out. Second, a CORS failure is not an HTTP error status. There is no CORS status code. The response may well have been 200; the browser simply did not hand it to your code, so the fetch Promise rejects and the explanation is printed in the console.
The public APIs used in this chapter — PokeAPI, JSONPlaceholder, and Open-Meteo — all send the header, so they work from a page. Who blocks what, how a server allows an origin, and what an OPTIONS preflight request is are covered in chapter 06, together with authentication.
Practice
Three tasks, all using real APIs that are free and need no account. You can run every one of them in the browser console.
Quiz
Eight questions, all about the traps in this chapter.
At the instant const p = fetch(url) runs, what is in the variable p?
When is res.ok true?
In which case does the Promise from fetch reject, so that execution goes straight to catch?
After const a = await res.json(); you write another line, const b = await res.json();. What happens on the second line?
You want to send a piece of JSON data to a server with fetch. Which of these are required? (Select all that apply.)
The method that turns a JavaScript object into JSON text is JSON.____ (9 letters).
fetch in the browser reports a CORS error for an API, but curl gets a normal response from the same address. Who stopped the data?
async function f() { return 42; }. You call f(). What do you get back?
- fetch returns a Promise immediately: an object standing for a result that has not arrived.
awaitpauses the surrounding async function until it settles, without blocking the browser. An async function always returns a Promise. - Two awaits, two steps. The first resolves when the response headers arrive.
res.json()is a second asynchronous step that reads and parses the body, and it can fail on its own when the body is not valid JSON. - fetch rejects only on a network-level failure. 404 and 500 are responses that arrived normally, so you check
res.okyourself. Keep the shape try → if (!res.ok) throw → catch. - Getting data onto the page is three steps:
fetch → res.json() → update the DOM. The body is a stream and can be read once, so store the result if you need it twice. - A POST needs three things:
method,Content-Type: application/json, andJSON.stringify(body). Leave one out and the data does not arrive as JSON. - When descriptions do not match, open the Network panel: Headers for the metadata, Payload for what you sent, Response for what came back, Timing for where the time went.
- A CORS error is the browser enforcing the same-origin policy. It is not an API failure, and it is not an HTTP status. Chapter 06 covers it.