Table of Contents#
- Understanding Node Fetch Basics
- The "Catch Block Isn’t Working" Scenario: A Common Pitfall
- Why Invalid JSON Causes Unhandled Rejections
- Step-by-Step Troubleshooting
- Best Practices to Prevent Unhandled Rejections
- Conclusion
- References
1. Understanding Node Fetch Basics#
Before diving into the problem, let’s recap how fetch works in Node.js. The fetch API returns a Promise that resolves to a Response object when the request completes (regardless of the HTTP status code, e.g., 404, 500). This is a critical detail: fetch only rejects on network failures (e.g., DNS errors, no connection), not on HTTP error statuses or invalid response bodies.
To extract data from the Response object, you typically use methods like response.json(), which parses the response body as JSON. However, response.json() itself returns a new Promise. If the response body is not valid JSON, this promise rejects, which can lead to unhandled rejections if not properly caught.
2. The "Catch Block Isn’t Working" Scenario: A Common Pitfall#
Let’s start with a familiar code snippet that seems correct but fails silently:
// Broken code: Catch block doesn't catch invalid JSON errors
fetch("https://api.example.com/data")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // Parsing JSON here
})
.then((data) => {
console.log("Success:", data);
})
.catch((error) => {
console.error("Fetch error:", error); // This catch block may not trigger!
});At first glance, this looks solid: we check for HTTP errors with response.ok, parse the JSON, and have a .catch() at the end. But if the API returns invalid JSON (e.g., a malformed string like { "name": "John", } with a trailing comma), you’ll see an "UnhandledPromiseRejectionWarning" instead of the expected "Fetch error" log.
Why? The fetch promise resolves successfully (network request worked), so the first .then() runs. Then response.json() is called, which returns a rejected promise (due to invalid JSON). But this rejection isn’t caught by the final .catch() because it occurs in a nested promise chain.
3. Why Invalid JSON Causes Unhandled Rejections#
To understand this, let’s break down the promise chain step by step:
Promise Flow:#
-
fetch(...)returns a promise (Promise A).- If the network fails,
Promise Arejects, and the final.catch()catches it. - If the network succeeds,
Promise Aresolves to aResponseobject.
- If the network fails,
-
The first
.then()receives theResponseand callsresponse.json(), which returns a new promise (Promise B).Promise Bresolves if the response is valid JSON.Promise Brejects if the response is invalid JSON (e.g., syntax error).
-
If
Promise Brejects and there’s no.catch()attached to it, the rejection propagates up the chain. However, if the final.catch()is only attached toPromise A, it may not catch rejections fromPromise B—especially in older Node.js versions or if the chain is broken.
Key Insight:#
Unhandled rejections occur because the error is thrown in Promise B (the response.json() call), not in Promise A (the initial fetch). The final .catch() is designed to catch errors from Promise A and its direct chain, but if Promise B rejects without a local .catch(), it becomes unhandled.
4. Step-by-Step Troubleshooting#
Let’s diagnose and fix the invalid JSON issue with a systematic approach.
Step 1: Identify the Source of the Error#
First, confirm whether the error is from fetch or response.json(). Add logs to isolate the problem:
fetch("https://api.example.com/data")
.then((response) => {
console.log("Fetch resolved with response:", response); // Log the response
return response.json();
})
.then((data) => console.log("Data parsed:", data))
.catch((error) => console.error("Caught error:", error));- If you see "Fetch resolved with response" in the logs, the network request succeeded, and the error is likely from
response.json(). - If you don’t see that log, the error is from
fetch(network failure).
Step 2: Inspect the Raw Response Body#
If the error is from response.json(), log the raw response body to check for invalid JSON:
fetch("https://api.example.com/data")
.then((response) => {
// Log raw response text before parsing
return response.text().then((text) => {
console.log("Raw response body:", text); // Check for invalid JSON here
return JSON.parse(text); // Explicitly parse to trigger error
});
})
.then((data) => console.log("Data:", data))
.catch((error) => console.error("Error:", error));Now you’ll see the raw text, which may reveal issues like:
- Trailing commas (e.g.,
{ "key": "value", }). - HTML error pages (e.g., a 500 error with
<html>...</html>instead of JSON). - Missing brackets/quotes (e.g.,
{ key: "value" }instead of{ "key": "value" }).
Step 3: Add Error Handling for response.json()#
The root fix is to explicitly handle rejections from response.json(). There are two common approaches:
Approach A: Use .catch() on the response.json() Promise#
Attach a .catch() directly to the response.json() call to handle parsing errors:
fetch("https://api.example.com/data")
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
// Handle JSON parsing errors here
return response.json().catch((jsonError) => {
throw new Error(`Failed to parse JSON: ${jsonError.message}`);
});
})
.then((data) => console.log("Success:", data))
.catch((error) => console.error("Handled error:", error)); // Now catches JSON errorsApproach B: Use async/await with try/catch (Cleaner!)#
async/await makes error handling more linear. Wrap both fetch and response.json() in a try/catch block:
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// Parse JSON with try/catch to handle invalid JSON
try {
const data = await response.json();
console.log("Success:", data);
return data;
} catch (jsonError) {
throw new Error(`JSON parse failed: ${jsonError.message}`);
}
} catch (error) {
console.error("Handled error:", error); // Catches all errors: fetch + JSON
}
}
fetchData();This ensures both network errors (from fetch) and parsing errors (from response.json()) are caught.
Step 4: Validate JSON Before Parsing#
For extra safety, validate the response content type and structure before parsing. For example:
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
// Check if response is JSON before parsing
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
throw new Error("Response is not JSON");
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error:", error);
}
}5. Best Practices to Prevent Unhandled Rejections#
To avoid invalid JSON-related unhandled rejections, follow these practices:
1. Always Handle response.json() Errors#
Whether using .then() chains or async/await, explicitly handle errors from response.json(). Never assume the response is valid JSON.
2. Use async/await for Readability#
async/await with try/catch makes it easier to handle nested promise errors (like response.json()) in a single block.
3. Validate HTTP Status Codes#
fetch doesn’t reject on 4xx/5xx statuses. Always check response.ok (or response.status) to throw errors for non-2xx statuses:
if (!response.ok) {
throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
}4. Log Raw Responses for Debugging#
When debugging, log the raw response body (with response.text()) to inspect invalid JSON or unexpected content.
5. Wrap Fetch in a Utility Function#
Create a reusable fetch wrapper to standardize error handling:
async function safeFetch(url) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const contentType = response.headers.get("content-type");
if (!contentType?.includes("application/json")) {
throw new Error("Response is not JSON");
}
return await response.json();
} catch (error) {
console.error("safeFetch error:", error);
throw error; // Re-throw to let callers handle if needed
}
}
// Usage
safeFetch("https://api.example.com/data")
.then((data) => console.log("Data:", data))
.catch((error) => console.error("Final error:", error));6. Conclusion#
The "Node fetch catch block isn’t working" issue often stems from misunderstanding how fetch and response.json() work together. Remember:
fetchresolves to aResponseobject even for non-2xx statuses (it only rejects on network failures).response.json()returns a promise that rejects if the body is invalid JSON.- Unhandled rejections occur when
response.json()rejects and no.catch()(ortry/catch) is attached to that specific promise.
By explicitly handling errors at each step of the promise chain—especially for response.json()—and validating responses, you can eliminate unhandled rejections and build more robust Node.js applications.