π¦ 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
Headersinterface - 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(), β¦).
Status Properties
Before touching the body, inspect the status. These read-only properties tell you how the request went:
| Property | Type | Meaning |
|---|---|---|
response.ok | boolean | true when status is 200β299 |
response.status | number | The numeric code: 200, 404, 500β¦ |
response.statusText | string | The text: "OK", "Not Found"β¦ |
response.url | string | Final URL after any redirects |
response.redirected | boolean | Whether a redirect occurred |
response.type | string | "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}`);
}
| Header | Why you'd read it |
|---|---|
Content-Type | Decide how to parse the body (JSON? image? text?) |
Content-Length | Total size β useful for a progress bar |
ETag / Last-Modified | Conditional 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.
// 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:
- If
!response.ok, build anErrorwhose message comes from the JSON body'smessage/errorfield when present, otherwise from the status. Attacherror.status. Throw it. - Otherwise, read the body based on
Content-Type: JSON βjson(), text βtext(), elseblob(). - Handle a
204 No Contentresponse by returningnullwithout trying to parse a body. - 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 Contentbefore parsing. - Extract error detail from the response body to give users a real message.
- Route body reading by
Content-Typewhen 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
Responsehas three parts: status, headers, and a single-use body. ok/statustell you success; headers carry metadata likeContent-TypeandETag.- 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; streamingresponse.bodyenables 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.