await fetch() isn't waiting for what you think it is.
August 9, 2025 · 4 min read

You've probably written code like this many number of times without ever asking why it needs two .then()s.
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => console.log(json))So, we are going to talk about one of the most common misunderstandings about the Fetch API, i.e. "fetch() returns the response body".
It doesn't.
And more importantly, it doesn't even wait for the response in the way most people assume it does.
When you write:
const response = await fetch("/api/user");You are not waiting for the full HTTP response body, and you are not even waiting for the full response to arrive.
You are only waiting for the browser to say:
"I've started the request, and I've received enough information to give you a Response object." - Interesting right, What even we used await for then?
Let me explain that
When we do: const response = await fetch("/api/user"); the browser immediately starts the network request in the background.
But the await does not mean: wait until the server responds fully or wait until JSON is downloaded.
Instead, it means wait until the browser has received the response metadata and can construct a response object.
At that point, we already have:
- HTTP status (200, 404, 500…)
- headers
- content-type
- a body reference
But importantly: we do not yet have the actual data.
The real surprise: the body is not data we assume to be
Inside the Response object, the body is not your JSON, text, or file.
It is a ReadableStream
Response {
status: 200,
statusText: "OK",
headers: Headers { ... },
body: ReadableStream { locked: false },
bodyUsed: false,
ok: true,
url: "https://api.example.com/user"
}So even after our fetch call we still haven't "gotten the response data" in the way most of us think too.
We have only gotten access to a stream that will eventually produce it.
Why this design exists
If fetch() waited for the full response body before resolving, many modern use cases would break or become inefficient like large file downloads, video streaming, ai token streaming responses, etc.
Instead, the browser resolves fetch() early so we can inspect headers immediately, decide how to consume the body and start processing data progressively if needed.
The second async step: consuming the stream
Once you have the Response, you still need to explicitly consume the body.
For JSON:
const data = await response.json();For text:
const text = await response.text();And this is where the second misconception appears: "Why is there another await after fetch()?"
Because this is a completely separate asynchronous process.
What response.json() is actually doing
When you call:
await response.json();the browser cant resolve immediately, It must:
- Read the entire response body stream until completion
- Decode the bytes into text (typically UTF-8)
- Parse the text as JSON
- Return the resulting JavaScript value
The takeaway
- fetch() is responsible for making the HTTP request and returning a Response.
- The Response gives you metadata immediately and exposes the body as a ReadableStream.
- Methods like json(), text(), and blob() are responsible for consuming that stream and converting it into the format your application needs.