Why a 204 Response Breaks response.json()
Understand why No Content responses cause JSON parsing failures and how to handle empty success responses without hiding real API bugs.
Quick Answer
A 204 response means the request succeeded and the server intentionally sent no response body. response.json() tries to parse the body as JSON, but there is nothing to parse, so it can throw Unexpected end of JSON input. Handle 204 before parsing and treat empty 200 responses separately.
Example Scenario
A DELETE request succeeds in the Network panel. The status is 204, the resource is removed, and the backend did exactly what it promised. The frontend still logs Unexpected end of JSON input because a shared API helper always calls response.json(), even for endpoints that return no body.
Step-by-Step Explanation
- Check whether the endpoint is documented to return 204 No Content.
- Branch on response.status === 204 before reading the body.
- Return null, undefined or a typed empty result from the API helper.
- Only parse JSON when a body exists and the Content-Type is JSON-compatible.
- Investigate empty 200 responses separately because they may indicate a server bug.
- Keep delete, update and command-style endpoints explicit about their response contract.
204 Is Not an Error
The important detail is that 204 is a successful status. It tells the client that the operation completed and there is no representation to return. That makes it a good fit for DELETE endpoints, idempotent commands and operations where the client already has enough state.
The parsing error appears because a client helper treats every response as if it has a JSON body. In that case the helper is too broad. The server is not required to send {} just to satisfy a generic parser.
This distinction is useful during incident review. A successful empty response means the transport and server operation may be fine. The bug is often in a client abstraction that forgot some successful responses are intentionally silent.
Separate Empty Success from Malformed JSON
Do not lump every Unexpected end of JSON input into the same bucket. A 204 response with no body is normal. A 200 response with an empty body may be suspicious if the API contract says it should return JSON. A truncated response after a network failure is another different case.
Your debugging code should record the status, Content-Type and raw text length. Those three facts separate intentional empty responses from malformed or missing payloads.
The easiest mistake is to look only at the exception message. The message describes what the JSON parser saw, not what the HTTP response meant. Two responses can produce the same parser message while requiring completely different fixes.
Shared API Helpers Need Response Contracts
A shared request helper is useful until it erases endpoint differences. Some endpoints return JSON resources. Some return problem+json errors. Some return 204. Some return files. A single helper can still handle these cases, but it needs explicit branching rather than one unconditional response.json() call.
A practical pattern is to centralize the boring checks while allowing callers to choose the expected response type. For example, a delete call can expect empty, while a profile call can expect json.
That choice should live close to the endpoint wrapper, not scattered across components. A button should not need to remember that DELETE /api/session returns no body. The API wrapper can express that once and give the rest of the application a simple result.
Check the Body Before Choosing a Policy
Before changing a shared helper, inspect the raw response for several endpoint types. Test a 204 delete, a normal JSON success, a validation error, an auth error and a server error. The helper should match the real API surface, not just the endpoint that exposed the bug today.
For a 204 response, reading response.text() should produce an empty string. For a JSON error, the text should contain an object with useful fields. For a file response, the content type and body handling should avoid JSON entirely. Seeing these cases side by side makes the policy obvious.
This review also prevents a common overcorrection: treating every empty text body as success. Empty 204 is expected. Empty 500 is still a broken server response. Empty 200 may be acceptable only when the endpoint contract says so.
Keep one captured example for each case so future helper changes can be checked against real responses instead of memory.
What to Return to Application Code
For 204 responses, application code usually benefits from a boring value such as null. That makes the absence of a body explicit without forcing every caller to handle a thrown parser error. In TypeScript, you can model this as Promise<null> or as a union when an endpoint may return either JSON or no content.
Avoid returning an empty object unless the API contract actually says the body is an object. Returning {} can hide the difference between “there was no body” and “the server returned an empty JSON object.”
For command-style endpoints, consider returning a typed result such as { ok: true } from the wrapper while still treating the HTTP body as empty. That gives UI code a stable signal without inventing a server payload that was never sent.
What to Check Next
If the 204 handling is correct but the UI still fails, inspect the code that consumes the result. A component may assume the delete call returns the deleted record or a refreshed list. The parsing issue may be solved, while the state update still needs a clearer contract.
If a server returns 204 with a Content-Type header or a body, clean that up too. A 204 response should not include a message body. Keeping the response minimal makes clients and proxies behave more predictably.
Finally, document the endpoint behavior in the same place developers look when adding a new call. The goal is not just to stop one exception. It is to stop the next shared helper from assuming every successful status has a JSON document behind it.
Code Examples
async function request(url, options) {
const response = await fetch(url, options);
return response.json(); // breaks for 204
} async function requestJson(url, options) {
const response = await fetch(url, options);
if (response.status === 204) {
return null;
}
const text = await response.text();
return text ? JSON.parse(text) : null;
} async function apiRequest(url, options, expected = 'json') {
const response = await fetch(url, options);
if (expected === 'empty' || response.status === 204) {
return null;
}
return response.json();
}
await apiRequest('/api/item/42', { method: 'DELETE' }, 'empty'); Common Mistakes
- Treating 204 as a failed request.
- Calling response.json() in a shared helper without checking status.
- Returning {} when the response actually had no body.
- Ignoring empty 200 responses that violate an API contract.
- Forgetting that response bodies can only be consumed once.
FAQ
Should a 204 response include an empty JSON object?
No. 204 means no response body. If the API should return JSON, use a different success status and document the body.
Is Unexpected end of JSON input always caused by 204?
No. It can also come from empty 200 responses, truncated bodies or reading the wrong response text.
What should my client return for 204?
Return a clear empty value such as null, or model the endpoint as an empty response in your request helper.