TypeError: Failed to execute 'json' on 'Response': body stream already read

Quick answer

A fetch Response body is a one-shot stream — you can read it only once. Calling .json() after a debug .text() (or reading it twice anywhere) throws this error. Fix it by reading the body once and storing the result, or call response.clone() before the first read if two consumers each need the stream.

The exact error string

const res = await fetch('/api/user');

console.log(await res.text());   // debug: read #1 drains the stream
const data = await res.json();   // read #2 ❌

// Chrome / V8:
//   TypeError: Failed to execute 'json' on 'Response': body stream already read
// Firefox:
//   TypeError: Response.json: Body has already been read
// Node (undici):
//   TypeError: Body is unusable: Body has already been read

Every wording points to the same thing: the body was consumed once already, so the second read has nothing left. The most common way people hit this is exactly the snippet above — adding a debug log and forgetting it eats the body.

Why a fetch body can only be read once

A Response body is a ReadableStream, not a stored string. Streaming is deliberate: it lets the browser hand you data as it arrives and process huge responses without buffering the whole payload in memory. But a stream flows in one direction and is consumed as it is read — once it is drained, it is gone.

The body-reading methods — .json(), .text(), .blob(), .arrayBuffer(), .formData() — each fully consume the stream. After any one of them runs, response.bodyUsed flips to true and every later read is rejected:

const res = await fetch('/api/user');
res.bodyUsed;            // false
const data = await res.json();
res.bodyUsed;            // true  — any further read now throws

Fix 1: read once, store the result (recommended)

In the overwhelming majority of cases you do not need to read the body twice — you need the data twice. Read it once and reuse the parsed value:

const res = await fetch('/api/user');
const data = await res.json();   // read exactly once

console.log(data);               // reuse the value, not the stream
useUser(data);
cache.set(url, data);

Fix 2: for debugging, read text once and parse it

The classic trigger is logging the raw body to debug, then calling .json(). Instead, read the text a single time and parse that string — you get both the log and the object from one read:

const res = await fetch('/api/user');
const raw = await res.text();    // one read
console.log('raw body:', raw);   // debug

const data = JSON.parse(raw);    // parse the string you already have

This also gives you a clearer error if the body is not valid JSON: you can see the exact raw text that caused an Unexpected token < in JSON or Unexpected non-whitespace character after JSON data before JSON.parse throws.

Fix 3: response.clone() when two consumers need the stream

If two independent pieces of code each genuinely need to read the body — for example a logging interceptor and your application code — clone the response before the first read. Each copy can be read once:

const res = await fetch('/api/user');

// Log the body without disturbing the original:
const logCopy = res.clone();
console.log('intercepted:', await logCopy.text());

const data = await res.json();   // original is still unread — works

Trade-off: clone() tees the stream, so the data is buffered until both copies are consumed (and you must consume both). For very large bodies that adds memory pressure — prefer Fix 1 unless you truly need two separate readers. Also note you must clone before reading; once bodyUsed is true, clone() cannot recover the data.

A common React / interceptor variant

The same error shows up when a shared layer reads the body and your component reads it again — an Axios-style fetch wrapper, a service worker, or a retry helper that peeked at the body. The rule is the same: whoever reads first must either clone for the next reader or pass the already-parsed value forward instead of the raw response.

// ❌ wrapper reads the body, then returns res for the caller to read again
async function http(url) {
  const res = await fetch(url);
  if (!res.ok) console.error(await res.text());  // drains the stream
  return res;                                     // caller's .json() now throws
}

// ✅ return the parsed data, or clone for the error path
async function http(url) {
  const res = await fetch(url);
  if (!res.ok) {
    const msg = await res.text();
    throw new Error(`HTTP ${res.status}: ${msg}`);
  }
  return res.json();                              // one read, value returned
}

Retrying a request: clone before the first read

A retry helper that needs to inspect the body to decide whether to retry must clone first — otherwise the body is consumed and the retry (or the success path) has nothing to read. The safe shape reads each response's body exactly once:

async function fetchWithRetry(url, tries = 3) {
  for (let i = 0; i < tries; i++) {
    const res = await fetch(url);
    if (res.ok) return res.json();          // consume once on success

    // peek at the error body without blocking a retry: read this response's
    // body once (it's a fresh Response each loop, so no reuse problem)
    const errText = await res.text();
    if (res.status < 500 || i === tries - 1) {
      throw new Error(`HTTP ${res.status}: ${errText}`);
    }
    await new Promise(r => setTimeout(r, 2 ** i * 200));   // backoff
  }
}

Each loop iteration gets a brand-new Response, so there is no cross-request reuse — the rule "one read per response" is still satisfied. You only need clone() when the same response must be read twice.

Node.js and server-side variants

The same one-shot-stream rule applies on the server, with different wording. In Node's fetch (undici) the message is TypeError: Body is unusable. And an incoming request body is also a stream you can only read once — a middleware that already consumed it leaves nothing for the next handler:

// Express: body-parser / express.json() already read the stream,
// so re-reading req as a stream yields nothing (or "stream is not readable").
app.use(express.json());                 // consumes the request body once
app.post('/x', (req, res) => {
  req.on('data', () => {});               // ❌ never fires — already consumed
  doSomething(req.body);                  // ✅ use the parsed body instead
});

The fix mirrors the browser case: read or parse the stream once, then pass the resulting value around — never re-read the stream.

Debugging checklist

Frequently Asked Questions

What does 'body stream already read' mean?

It means you tried to read a fetch Response body more than once. The body is a one-shot ReadableStream — once .json(), .text(), .blob(), .arrayBuffer(), or .formData() consumes it, the stream is drained and any second read throws TypeError: Failed to execute 'json' on 'Response': body stream already read.

Why can a fetch body only be read once?

The body is a stream, not a stored string. Streaming lets the browser process large responses without holding the whole thing in memory, so the data flows through once and is gone. The boolean response.bodyUsed becomes true after the first read, and further reads are rejected.

How do I read a fetch response body twice?

Call response.clone() before the first read and read each copy once, or — more commonly — read the body a single time and store the result in a variable to reuse. Cloning is for when two independent consumers each need the stream; storing the parsed value is simpler when you just need the data more than once.

I added console.log(await res.text()) and now .json() fails — why?

That debug log consumed the body stream, so the following res.json() has nothing left to read and throws 'body stream already read'. Either log res.clone().text() so the original is untouched, or read res.text() once into a variable and JSON.parse it instead of calling res.json().

Does the error wording change between browsers and Node?

Yes. Chrome says "Failed to execute 'json' on 'Response': body stream already read", Firefox says "Body has already been consumed" or "Response.json: Body has already been read", and Node/undici says "Body is unusable" or "already read". They all mean the body stream was consumed twice.

Does response.clone() copy the data twice in memory?

clone() creates a second readable stream that tees off the original, so both copies must be consumed and the data is effectively buffered until both are read. For very large bodies, prefer reading once and reusing the parsed result rather than cloning, to avoid extra memory pressure.

Logged the body and it wasn't JSON?

Paste the raw response text into the formatter to see exactly what the server returned — nothing is uploaded to a server.

JSON Formatter JSON Validator All Error References
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.