Skip to content

Make json() throw on empty responses#854

Merged
sholladay merged 1 commit intomainfrom
json-throw
Apr 2, 2026
Merged

Make json() throw on empty responses#854
sholladay merged 1 commit intomainfrom
json-throw

Conversation

@sindresorhus
Copy link
Copy Markdown
Owner

This makes the default .json() contract simpler and more predictable.

Returning undefined for empty 200 / 204 responses was type-corrrect, but it pushed a rare edge case into every normal JSON call site by widening .json<T>() to Promise<T | undefined>. That made the common path worse for everyone just to accommodate a response shape that is usually either a server bug or a different API contract.

This change restores a cleaner boundary:

  • plain .json() means “parse JSON”
  • empty responses are treated as not-JSON and throw
  • the return type goes back to Promise<T>

That is better because it gives us strict runtime behavior, simple types, and no global | undefined pollution.

@sholladay
Copy link
Copy Markdown
Collaborator

So basically, the behavior is identical to fetch, except that schemas can be used to handle empty bodies as undefined and convert them to something else? That seems reasonable. I also like that parseJson is protected from handling empty strings, that's nice.

The only thing I'm not sure of is the 204 case. Is throwing better than returning an empty string like we did in 1.x? If so, how should users handle responses where they expect either JSON or an empty 204? They could catch the ky() call for when the parser throws, but there is no error.response.status for them to check, which they would need to distinguish a 204 from other kinds of invalid JSON.

One solution could be a dedicated ParseError class as discussed in #605.

Another solution could be an afterResponse hook that transforms 204 bodies to, say, an empty object, making them valid JSON for the parser.

@sindresorhus
Copy link
Copy Markdown
Owner Author

I think throwing is still the better default for 204.

For endpoints that intentionally return either JSON or 204 (which I would argue is an anti-pattern and probably not happening much in the wild), I think the cleaner pattern is to await the response first and branch on status before parsing:

const response = await ky(url);

if (response.status === 204) {
	return undefined;
}

return response.json();

That avoids needing to catch a parse error just to inspect status, and it keeps plain .json() as a strict “pars JSON” operation.

I think a dedicated ParseError could still be useful as a separate improvement for observability / ergonomics, but I would not block this change on it.

@sholladay sholladay merged commit 1b8e1ff into main Apr 2, 2026
6 checks passed
@sholladay sholladay deleted the json-throw branch April 2, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants