If you’ve worked with the Fetch API in JavaScript, you’ve probably seen this pattern:
const response = await fetch(url);
const data = await response.json();
At first glance, it seems like we’re doing two separate awaits for one operation. So what’s going on under the hood?
Let’s break it down.
🚀 The Two Awaits Explained
1. await fetch(url)
This sends the request and waits for the HTTP response headers to arrive.
- ✅ The request is sent.
- ✅ Headers (status, content type, etc.) are received.
- ❌ The body is not yet read or downloaded.
This gives you a Response
object, which you can inspect before deciding what to do next.
2. await response.json()
(or .text()
, .blob()
, etc.)
This reads and parses the response body.
- ✅ Downloads the body stream.
- ✅ Parses it according to the method you choose (e.g., JSON, text, Blob).
- ❌ Doesn’t happen automatically—you must initiate it.
🧠 Why Are They Separate?
This design is intentional. The Fetch API gives you:
- Fine-grained control over network operations.
- The ability to check the response status or headers before downloading the body.
- Better performance and resource usage, especially for large or streaming data.
Example:
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json(); // Only parse if OK
🤔 What If You Skip response.json()
?
Great question.
If you only do:
const response = await fetch(url);
And don’t follow up with .json()
or .text()
:
- The body is not downloaded.
- The stream remains open, which can lead to memory leaks or unused network connections.
- The browser won’t read or parse anything further unless you explicitly do it.
🤔 How to verify this?
You can check by opening the network tab. You will see a blank screen in the response tab.
🤔 How to Cancel/Close a Fetch Response (If You Don’t Want the Body)
✅ Option 1: Use response.body.cancel()
const response = await fetch(url);
if (!response.ok) {
// Cancel the stream if you're not going to read it
response.body?.cancel();
throw new Error(`Request failed with status ${response.status}`);
}
✅ Option 2: Use AbortController to Cancel Entire Request
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) {
response.body?.cancel();
throw new Error(`Error: ${response.status}`);
}
const data = await response.json();
clearTimeout(timeoutId);
console.log(data);
} catch (err) {
console.error("Fetch aborted or failed", err);
}
✅ Quick Summary
Action | Body Downloaded? | Body Parsed? |
---|---|---|
await fetch(url) | ❌ (just headers) | ❌ |
await response.json() | ✅ | ✅ (as JSON) |
await response.text() | ✅ | ✅ (as text) |
No body read at all | ❌ | ❌ |
🏁 Conclusion
The two-step await fetch()
+ await response.json()
pattern is by design. It provides flexibility, improves efficiency, and avoids unnecessary processing when you don’t need the response body.
Next time you use fetch
, you’ll know exactly what’s happening—and when!