Skip to main content

πŸ“¦ Response Processing and Handling

A request is only half the story. When the server replies, fetch hands you a Response object packed with status, headers, and a body stream. This lesson shows you how to inspect it, read the body in the right format, distinguish network failures from HTTP errors, and handle the tricky edges like single-use bodies and streamed downloads.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Read a Response's status properties β€” ok, status, statusText, url, redirected
  • Inspect response headers with the Headers interface
  • Pick the right body method and route by Content-Type
  • Distinguish network errors from HTTP status errors and handle both
  • Use response.clone() and read the body stream for download progress

Estimated Time: 35–45 minutes  β€’  Difficulty: Intermediate

Hands-on: Write a content-type–aware handleResponse() that returns the correctly-parsed data or a rich error.

In This Lesson

The Response Object

Every fetch resolves to a Response. It's a structured view of what the server sent back, split into three areas: status (did it work?), headers (metadata about the reply), and the body (the actual payload).

πŸ’‘ An analogy: A response is like a package on your doorstep. The tracking status tells you it arrived and whether delivery succeeded (status/ok). The shipping label lists the sender, weight, and contents type (headers). And inside is the thing you actually ordered β€” which you unwrap in whatever way suits it (json(), blob(), …).
The anatomy of a Response object A Response object splits into status properties, headers, and body-reading methods. Response Status ok Β· status statusText url Β· redirected Headers .get() Β· .has() Content-Type ETag Β· Cache-Control Body json() Β· text() blob() Β· formData() arrayBuffer()
Figure 1 β€” A Response has three parts. Check the status first, consult headers when you need metadata, then read the body exactly once with the method that matches the content.

Status Properties

Before touching the body, inspect the status. These read-only properties tell you how the request went:

PropertyTypeMeaning
response.okbooleantrue when status is 200–299
response.statusnumberThe numeric code: 200, 404, 500…
response.statusTextstringThe text: "OK", "Not Found"…
response.urlstringFinal URL after any redirects
response.redirectedbooleanWhether a redirect occurred
response.typestring"basic", "cors", "opaque"…
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');

console.log(response.status);     // 200
console.log(response.statusText); // "OK"
console.log(response.ok);         // true
console.log(response.url);        // the final URL
console.log(response.redirected); // false

πŸ’‘ Why ok exists

Without response.ok you'd write response.status >= 200 && response.status < 300 on every request. The boolean is just a convenient shorthand for that exact range.

Reading Headers

Response headers arrive as a Headers object with a Map-like interface. Header names are case-insensitive.

const response = await fetch('https://api.example.com/data');

// Look up individual headers
response.headers.has('Content-Type');        // true / false
response.headers.get('Content-Type');        // "application/json; charset=utf-8"

// Iterate over every header
for (const [name, value] of response.headers.entries()) {
  console.log(`${name}: ${value}`);
}
HeaderWhy you'd read it
Content-TypeDecide how to parse the body (JSON? image? text?)
Content-LengthTotal size β€” useful for a progress bar
ETag / Last-ModifiedConditional requests and caching
X-RateLimit-*Track how much API quota remains

⚠️ You can't read every header cross-origin

For cross-origin responses, only CORS-safelisted headers are visible unless the server explicitly exposes more via Access-Control-Expose-Headers. A custom header like X-Total-Count may read as null until the server opts to expose it.

Body Processing Methods

The body is a stream. You turn it into usable data by calling one of the reader methods β€” each returns a promise and consumes the stream once.

flowchart LR A[Response body stream] --> B["json()"] A --> C["text()"] A --> D["blob()"] A --> E["formData()"] A --> F["arrayBuffer()"] B --> B1[JS object / array] C --> C1[String] D --> D1[Blob] E --> E1[FormData] F --> F1[ArrayBuffer]
// JSON API response
const users = await (await fetch('/api/users')).json();

// Plain text or HTML
const html = await (await fetch('/page.html')).text();

// Binary β€” image, PDF, etc.
const blob = await (await fetch('/logo.png')).blob();
imgEl.src = URL.createObjectURL(blob);

⚠️ A body can only be read once

The body is a one-shot stream. Calling two readers on the same response β€” say json() then text() β€” throws "body stream already read." If you need it twice, clone() the response first (next section).

Routing by Content-Type

When you don't know the format in advance, inspect the header and branch:

async function readByType(response) {
  const type = response.headers.get('Content-Type') || '';
  if (type.includes('application/json')) return response.json();
  if (type.includes('text/'))            return response.text();
  if (type.includes('image/'))           return response.blob();
  return response.blob(); // safe default for unknown binary
}

Two Kinds of Errors

Robust response handling means treating two very different failure modes correctly:

πŸ“– Network errors vs HTTP status errors

Network error: the request never completed β€” no connection, DNS failure, CORS block, or you aborted it. This rejects the fetch promise, landing in your catch.

HTTP status error: the server replied, but with a 4xx or 5xx code. This resolves the promise β€” you must detect it via response.ok.

A comprehensive handler covers both, and enriches the error with detail from the response body when the API provides it:

async function fetchWithErrorHandling(url, options) {
  let response;
  try {
    response = await fetch(url, options);
  } catch (networkError) {
    // Promise rejected -> genuine network/CORS/abort failure
    throw new Error(`Network error: ${networkError.message}`);
  }

  if (!response.ok) {
    // Server answered with 4xx/5xx. Try to pull a message from the body.
    let detail = `${response.status} ${response.statusText}`;
    try {
      const data = await response.json();
      detail = data.message || data.error || detail;
    } catch {
      // body wasn't JSON β€” keep the status-based message
    }
    const error = new Error(detail);
    error.status = response.status;
    throw error;
  }

  return response.json();
}

βœ… The mental model

Wrap the fetch() call itself to catch network failures, then check response.ok for HTTP failures. Two guards, two failure modes β€” no gaps.

Cloning & Streaming

Cloning a response

Because the body is single-use, response.clone() gives you a second readable copy β€” handy when you want to both use a response and stash it in a cache.

const response = await fetch('/api/data');
const copy = response.clone();

const data = await response.json();   // read the original
await caches.open('v1').then((c) => c.put('/api/data', copy)); // store the copy

Streaming with progress

For large downloads, read the body stream chunk by chunk to report progress β€” something the one-shot body methods can't do on their own. Combine Content-Length with the stream reader:

async function downloadWithProgress(url, onProgress) {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);

  const total = Number(response.headers.get('Content-Length')) || 0;
  const reader = response.body.getReader();
  const chunks = [];
  let received = 0;

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks.push(value);
    received += value.length;
    if (total) onProgress(Math.round((received / total) * 100));
  }

  return new Blob(chunks); // reassemble the downloaded bytes
}

πŸ’‘ When to bother

Reserve manual streaming for big files where a progress bar matters. For ordinary JSON API calls, response.json() is simpler and plenty fast.

Hands-on Exercise

πŸ‹οΈ Write a smart handleResponse()

Objective: Produce one function that takes a Response and returns correctly-parsed data on success, or throws a detailed error on failure β€” the workhorse you'd drop into any API client.

Instructions:

  1. If !response.ok, build an Error whose message comes from the JSON body's message/error field when present, otherwise from the status. Attach error.status. Throw it.
  2. Otherwise, read the body based on Content-Type: JSON β†’ json(), text β†’ text(), else blob().
  3. Handle a 204 No Content response by returning null without trying to parse a body.
  4. Test against https://jsonplaceholder.typicode.com/posts/1 (success) and a 404 path (failure).
πŸ’‘ Hint

Check response.status === 204 before attempting any body read β€” a No-Content response has no body, and calling json() on it will throw.

βœ… Sample solution
async function handleResponse(response) {
  if (!response.ok) {
    let message = `${response.status} ${response.statusText}`;
    try {
      const data = await response.json();
      message = data.message || data.error || message;
    } catch {
      // non-JSON error body; keep status message
    }
    const error = new Error(message);
    error.status = response.status;
    throw error;
  }

  if (response.status === 204) return null; // No Content

  const type = response.headers.get('Content-Type') || '';
  if (type.includes('application/json')) return response.json();
  if (type.includes('text/'))            return response.text();
  return response.blob();
}

// Usage
try {
  const post = await handleResponse(
    await fetch('https://jsonplaceholder.typicode.com/posts/1')
  );
  console.log(post.title);
} catch (error) {
  console.error(`Failed (${error.status}):`, error.message);
}

🎯 Quick Quiz

Question 1: Which situation causes the fetch() promise itself to reject (land in catch)?

Question 2: You need to both parse a response as JSON and store the raw response in a cache. What makes that possible?

Question 3: To show a download progress bar for a large file, what do you read?

Best Practices

βœ… Do

  • Check status before reading the body, and guard 204 No Content before parsing.
  • Extract error detail from the response body to give users a real message.
  • Route body reading by Content-Type when the format isn't guaranteed.
  • clone() when a response must be read more than once.

⚠️ Don't

  • Don't read the body twice without cloning β€” it throws.
  • Don't assume a custom cross-origin header is readable; the server must expose it.
  • Don't hand-roll streaming for small JSON β€” it's needless complexity.

Summary & Quiz

πŸŽ‰ Key Takeaways

  • A Response has three parts: status, headers, and a single-use body.
  • ok/status tell you success; headers carry metadata like Content-Type and ETag.
  • Pick the body reader that matches the content β€” each consumes the stream once.
  • Handle both network rejections and HTTP-status failures.
  • clone() enables double reads; streaming response.body enables progress.

πŸ“š Further Reading

πŸš€ What's Next?

You can now send requests and process what comes back. The next lesson zooms in on error handling strategies β€” building resilient code with retries, timeouts, and graceful fallbacks for when things go wrong.

πŸŽ‰ Great work!

Requests out, responses in, errors handled. You've got the full fetch round trip under control.