JavaScript's fetch
API is widely used for making HTTP requests, but it can be a bit tricky to understand why it sometimes requires two await
statements. If you've worked with fetch
before, you might have encountered code like this:
const response = await fetch('https://api.example.com/data');
const data = await response.json();
Let’s break this down and understand why this pattern is necessary. 🧐
The Two-Step Process of Fetch 🛠️
The fetch
API is designed to handle network requests asynchronously, but its behavior is split into two main stages:
-
Fetching the Response 🌐
- When you call
fetch
, it returns aPromise
that resolves to theResponse
object once the network request is completed. - This step doesn’t process the body of the response; it only ensures that the request was successful and headers are available.
- When you call
-
Reading the Response Body 📄
- The
Response
object has methods like.json()
,.text()
, and.blob()
to read the actual content. - These methods also return Promises because reading the body is asynchronous. This is necessary to handle large payloads efficiently without blocking the main thread.
- The
What Happens During the First await
? ⏳
When you write const response = await fetch(url);
, here’s what happens:
-
Network Request Sent: 🚀
- The browser initiates an HTTP request to the specified URL.
- This involves resolving the domain name, establishing a TCP connection, and sending the HTTP headers and body (for POST requests).
-
Response Metadata Received: 📬
- The
fetch
call resolves once the server responds with the status line (e.g.,HTTP/1.1 200 OK
) and headers. At this point:- The
status
(e.g., 200, 404, or 500) andstatusText
(e.g., "OK" or "Not Found") are available. - Response headers like
Content-Type
,Content-Length
, and any custom headers sent by the server are accessible.
- The
- The
-
Response Object Created: 🛠️
- The browser constructs a
Response
object that contains metadata about the response. This includes:-
Headers: Accessible via
response.headers
, which allows you to inspect specific headers likeContent-Type
orAuthorization
. - Body: At this point, the body has not been fully read or parsed—it remains as a readable stream.
-
Headers: Accessible via
- The browser constructs a
For example, if the server returns:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 123
{"message": "Hello, world!"}
The Response
object will contain:
-
status
: 200 ✅ -
statusText
: "OK" ✅ -
headers
: An iterable collection of the response headers (e.g.,Content-Type: application/json
). -
body
: A readable stream that hasn’t been parsed yet.
What Happens During the Second await
? 🔄
When you write const data = await response.json();
, the following steps occur:
-
Body Stream Read: 📥
- The body of the response (still in raw form) is read as a stream. Depending on the method you use, the raw data is processed accordingly:
-
.json()
: Parses the stream as JSON and returns a JavaScript object. -
.text()
: Reads the stream as a plain text string. -
.blob()
: Reads the stream as a binary large object.
-
- The body of the response (still in raw form) is read as a stream. Depending on the method you use, the raw data is processed accordingly:
-
Parsing and Resolving: 🧩
- The
json()
method parses the raw data (e.g.,{"message": "Hello, world!"}
) into a usable JavaScript object (e.g.,{ message: "Hello, world!" }
). - This parsing process is asynchronous because it involves processing potentially large data.
- The
-
Promise Resolution: ✅
- The
Promise
returned byresponse.json()
resolves to the parsed data, which can then be used in your application.
- The
Why Two await
Statements Are Needed 🤔
Here’s the reason you need await
twice:
-
First
await
(Waiting for the Response):- The
fetch
call doesn’t immediately provide the response data; it gives you aPromise
. You need toawait
it to get theResponse
object.
- The
-
Second
await
(Parsing the Body):- The
.json()
method (or other body-reading methods) returns anotherPromise
. You need toawait
this to extract the parsed content.
- The
If you skip either await
, you’ll likely end up with unexpected behavior:
-
Skipping the first
await
: You’ll be working with the unresolvedPromise
instead of the actualResponse
object. -
Skipping the second
await
: You’ll get aPromise
instead of the parsed data.
Example with Error Handling 🛡️
Here’s how you might handle errors properly while working with fetch
:
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error fetching data:', error);
}
Common Pitfalls ⚠️
-
Not Handling Errors:
-
fetch
doesn’t throw an error for HTTP errors like 404 or 500. You must checkresponse.ok
orresponse.status
manually.
-
-
Skipping the Second
await
:- Forgetting to
await
.json()
can lead to bugs where you’re working with a Promise instead of actual data.
- Forgetting to
-
Confusion Between
fetch
and Older APIs:- Developers transitioning from older APIs like
XMLHttpRequest
might expect synchronous behavior, butfetch
is entirely Promise-based.
- Developers transitioning from older APIs like
Conclusion 🎯
Using two await
statements with fetch
might seem redundant at first, but it’s a logical outcome of its asynchronous design. The first await
ensures the response has been received, including headers and metadata, while the second await
processes the response body. Understanding this flow helps you write more reliable and maintainable asynchronous code. 🚀
Top comments (2)
Interesting article, it is a question that I have in mind also. The step-by-step explanation of the two stages—fetching the response and reading the body—is super clear and helpful for understanding how the API works under the hood.
With fetch being so widely used, do you think there’s room for improvement in simplifying this two-step process, or does the current design strike the right balance between flexibility and simplicity?
I learned a kind of basic knowledge before Chrismas. Thanks.