"Unexpected End of JSON Input"

This is not the same bug as "Unexpected token in JSON". That one means the parser found something it didn't expect partway through a document. This one means it ran out of document before finding anything at all — JSON.parse('') and JSON.parse('{"a":') both throw exactly this, for the same underlying reason: there's less input than a complete JSON value requires.

reference guide JavaScript fetch API

// where this actually comes from in practice

Almost nobody hits this by calling JSON.parse() directly on an empty string on purpose — it shows up through fetch(), where response.json() calls JSON.parse() internally on whatever the body turned out to be.

the three scenarios that actually produce this
204 No Contentthe request succeeded and deliberately has no body — common on DELETE endpoints and some PUT/POST responses. Calling .json() on it parses an empty string
A network failure mid-responsethe connection dropped before the body finished arriving — the parser has a legitimate JSON prefix and then nothing, which is exactly "ran out of input"
An endpoint that just doesn't return a body on errora 500 or timeout where the server sends headers but no content — different from a server sending an HTML error page, which instead throws "Unexpected token <", not this
what actually happens, step by step
the code
const res = await fetch("/api/widgets/42", { method: "DELETE" });
const data = await res.json();  // res.json() calls JSON.parse() internally
what happens
SyntaxError: Unexpected end of JSON input
why
A successful DELETE conventionally returns 204 No Content -- no body at all. res.json() still tries to parse the (empty) body, which is the same as calling JSON.parse("").
the fix
const res = await fetch("/api/widgets/42", { method: "DELETE" });
const text = await res.text();          // read as text first, never throws
const data = text ? JSON.parse(text) : null;  // only parse if something's actually there
The general pattern for any fetch() call, not just DELETE: read the body as text once, check it's non-empty, then parse. This also means you keep the raw text to log when something does go wrong, instead of losing it the moment a failed .json() call throws.

// try it yourself

Paste a response body into the JSON Formatter to see exactly what's actually in it before deciding how to handle the empty/partial case.