Kartik Goel

await fetch() isn't waiting for what you think it is.

August 9, 2025 · 4 min read

await fetch() isn't waiting for what you think it is.

You've probably written code like this many number of times without ever asking why it needs two .then()s.

Classic Fetch Pattern
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:

Basic Fetch Request
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 Object Structure
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:

Consuming JSON
const data = await response.json();

For text:

Consuming 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:

Parsing Stream as JSON
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.