skip to content

Why can the body of a Response returned by fetch() be read only once — so a second call to response.json() throws — and what do you do when two pieces of code both need that body?

level: middleimportance: should knowfreq 48%

answer

  1. the body is a stream, not a string
  2. one pass, then it is spent
  3. bodyUsed flips after the first read
  4. tee it before anyone drinks
  5. only the body is consumed, not the metadata

basics

~20 s

A fetch Response body is a one-shot stream, not a stored string. The first read consumes it and sets response.bodyUsed to true, so a second read throws a TypeError. Call response.clone() before reading if two consumers need it.

solid answer

~50 s

`Response` (and `Request`) carry their body as a `ReadableStream`, so `json()`, `text()`, `blob()`, `arrayBuffer()` and `formData()` are all *consuming* reads of the same single source. The first one locks and drains the stream and flips `response.bodyUsed` to `true`; any second read rejects with a `TypeError` about the body already being read. That design is deliberate — the browser cannot buffer every response in memory just in case you read it twice, and streaming a large download would be impossible if it did. When two consumers need the body, call `response.clone()` **before** any read; the clone tees the stream so both copies are readable. Cloning is not free: the browser must buffer for whichever branch reads slower, so a clone nobody drains holds memory. The everyday alternative is simpler — read once into a variable and pass the value around.

code

javascript · 8 lines
javascript
async function fetchAndCache(url, cache) {
  const response = await fetch(url);
  // clone BEFORE any read; cloning a used body throws a TypeError
  const copyForCache = response.clone();
  const data = await response.json();
  await cache.put(url, copyForCache);
  return data;
}

go deeper

for a junior

Know that reading a fetch response body consumes it: after await response.json(), another read throws. Read it once into a variable and pass that value around.

for a middle

Explain that the body is a ReadableStream and every Body method drains it, that bodyUsed records this, and that clone() must be called before the first read. Be able to say why the platform chose one-shot bodies.

for a senior

Show judgment about clone() — it tees the stream and buffers for the slower branch, so an unread clone holds memory. Point out that most clone usage in application code is really a value that should have been read once and shared.

for a principal

Set the convention for how responses cross layer boundaries: pass parsed values, not Response objects, so no layer can consume another's body. Where cloning is genuinely needed, such as caching in a service worker, make draining both branches an explicit requirement.

## Bodies are streams, not strings Both `Response` and `Request` expose the same set of body-reading methods, sometimes called the Body mixin: `arrayBuffer()`, `blob()`, `formData()`, `json()` and `text()`. It is tempting to read that list as five different accessors over some stored payload. It is not. There is exactly one payload, exposed as `response.body`, a `ReadableStream`, and every one of those methods is a *consuming* read of it: it locks the stream, drains it to the end, and converts the accumulated bytes into the requested shape. Once that has happened, `response.bodyUsed` is `true` and the stream is spent. A second read — whether it is the same method again or a different one — rejects with a `TypeError`. Browsers phrase it slightly differently ("body stream already read", "Body has already been consumed"), but the cause is identical. ```js const response = await fetch('/api/items'); const text = await response.text(); // drains the stream console.log(response.bodyUsed); // true await response.json(); // TypeError: body already read ``` ## Why the platform does not just buffer everything The body may be a hundred bytes of JSON or a two-gigabyte video, and the browser often does not know which until the bytes stop arriving. If every response were retained so it could be read repeatedly, a page that downloaded a large file would have to hold it entirely in memory even when the code streamed it straight to disk. Making the body one-shot lets the browser hand bytes to the consumer and forget them, which is what makes streaming downloads possible at all. It also removes an ambiguity: with a single-pass stream there is never a question of whether `json()` re-parses the same bytes or re-reads the network. ## clone(), and its cost `response.clone()` produces a second `Response` that reads the same underlying data. The rule is that you must call it *before* the body is touched — calling `clone()` when `bodyUsed` is already `true` throws a `TypeError`. ```js const response = await fetch('/api/items'); const forCache = response.clone(); const data = await response.json(); // one branch parsed await cache.put('/api/items', forCache); // other branch still readable ``` Under the hood the clone tees the stream into two branches. Because the two consumers can read at different speeds, the browser must buffer everything the faster branch has consumed but the slower one has not. A clone that is created and then never read is therefore a memory leak in slow motion: the buffer grows to the whole body and is held until both branches are released. The classic legitimate use is the service worker one — answer the page from the network response and simultaneously put a copy in the `Cache` — and there both branches are consumed promptly. ## The same rule applies to Request A `Request` built with a body behaves identically: it has `bodyUsed`, it is drained when it is sent, and `request.clone()` must be taken while it is still pristine. This is why passing one `Request` object to `fetch()` twice fails on the second call. ## The pattern you should reach for first Most of the time cloning is a workaround for the real problem: reading the body in two places instead of one. Read it once, keep the value, share the value. ```js const response = await fetch(url); const raw = await response.text(); let data; try { data = JSON.parse(raw); } catch { throw new Error(`Non-JSON response (${response.status}): ${raw.slice(0, 200)}`); } ``` This shape also solves a common logging problem. Code that wants to log the raw body *and* parse it does not need `clone()` at all — one `text()` read gives you both, because `JSON.parse` operates on the string you already hold. Reaching for `clone()` there is a sign the reading and the parsing have been split across layers that should have been given the value instead of the `Response`. ## Things that are still fine after a read Only the body is spent. `status`, `ok`, `headers`, `url`, `redirected` and `type` remain readable on a consumed `Response` indefinitely, so an error wrapper can safely read the body and still report the status afterwards.

  • What happens if you call response.clone() after you have already awaited response.json()?
    It throws a `TypeError`. Cloning requires an undisturbed body, and `json()` has already locked and drained the stream, so there is nothing left to tee. The clone must be taken while `response.bodyUsed` is still `false` — in practice, on the line right after the `await fetch(...)` resolves.
  • Is a cloned Response free, or does it cost something?
    It costs memory. The two branches read the same data at their own pace, so the browser buffers whatever the faster consumer has taken and the slower one has not. A clone that is never read grows that buffer to the full body and holds it. Clone only when both copies will actually be consumed.
  • After you have read the body, can you still inspect anything on the Response?
    Yes — everything except the body. `status`, `ok`, `statusText`, `headers`, `url`, `redirected` and `type` are ordinary properties and stay readable forever. Only the stream is one-shot, which is why an error path can safely read the body first and still report the status when it throws.
  • Does the same one-shot rule apply to a Request object?
    Yes. `Request` carries the same body machinery, including `bodyUsed` and `clone()`. Sending it drains the body, so handing the same bodied `Request` to `fetch()` a second time fails. Clone it beforehand, or build a fresh `Request` for each send.

saying these in an interview costs you the question

  • Thinks the body is cached and re-readable like a string
  • Calls clone() after already awaiting json()
  • Uses clone() to log a body instead of reading text once
  • Believes the whole Response is unusable after a read
  • Assumes clone() copies nothing and costs nothing

context