Why Your JSON Parse Failed, and Why Decoding a JWT Isn't Verifying It
The API returned 200. Your frontend is dead anyway.
SyntaxError: Unexpected token } in JSON at position 118
Somewhere between the server and your parser there is a payload that looks like JSON, reads like JSON, and is not JSON. Here is how to find the break quickly, plus the JWT version of the same trap, because the two failures travel together in real debugging sessions.
Read the error as coordinates, not as an insult
A JSON parse error gives you a position. Use it. In Python the message is wordier and even more helpful:
import json
json.loads(payload)
json.decoder.JSONDecodeError: Expecting property name
enclosed in double quotes: line 4 column 3 (char 61)
Line 4, column 3 is where the parser gave up, and the actual mistake usually sits a character or two before it. Open the payload, go to that line, and look left. Nine times out of ten you will see one of the usual suspects.
The usual suspects, all in one payload
{
'user': 'ada',
"roles": ["dev", "admin",],
"score": undefined,
}
That short block contains three separate failures:
Single quotes. JSON strings need double quotes. 'ada' is JavaScript, not JSON.
A trailing comma. "admin",] and the comma after undefined are both legal in modern JavaScript objects. In JSON they end the parse.
An unquoted literal. undefined and NaN do not exist in JSON. Use null, or leave the key out.
Two more worth memorising because they pass a quick visual scan: comments (// looks fine) aren't allowed, and neither are unquoted keys. If you generated the payload by hand, or a template engine did, check those first.
Pasting the payload into a formatter is the fastest way to see the break in context. I use the JSON Formatter on a site I maintain, because it shows a tree view next to the raw text and the error position stops being abstract. Any formatter you trust does the same job. The point is seeing the structure, not squinting at a 400-character one-line string from a log file.
Now the token that "looks fine"
A JWT is three base64url segments joined by dots:
header.payload.signature
Decoding the first two segments is trivial, and you should know how to do it locally:
const decodePart = (part) =>
JSON.parse(atob(part.replace(/-/g, "+").replace(/_/g, "/")));
const [header, payload] = token.split(".").slice(0, 2).map(decodePart);
Watch the padding, by the way. Base64url drops the trailing = characters, and strict decoders in some environments will reject a segment whose length is not padded back to a multiple of 4. Browser atob usually copes. Your mileage will vary, quietly, at the worst moment.
Reading the payload answers the questions that cause most 401s:
exp is in seconds, not milliseconds. Compare it against Date.now() / 1000. Half the "token randomly rejected" bugs I have chased in discussions and issues came down to a unit mismatch or a server clock a few minutes adrift.
iss and aud must match what the API expects, exactly. A token minted for one environment fails against another with no useful error message.
alg in the header tells you what the issuer claims to have signed with. If it says none on a token guarding anything real, stop and go and read your auth library's defaults.
Decoding is not verifying. Say it twice.
Everything above reads the token. None of it proves the token is genuine, because the payload is not encrypted. Anyone holding the string can read every claim in it, and anyone can rewrite those claims, set "role": "admin", and produce a perfectly well-formed token. Only the signature check, done by your server with the issuer's key, separates a real token from a typed one.
Two habits follow from that:
Never put anything secret in a payload. Treat every claim as printed on a postcard.
Never paste a live production token into a random website. For a throwaway dev token, a browser decoder like this JWT Decoder saves a minute. For anything real, decode locally with the snippet above. A token is a credential, and credentials do not go into strangers' text boxes. That includes mine.
Regex: fine for shape, useless for parsing
You'll be tempted to validate payloads with regex. Resist, mostly. JSON needs a parser, and a regex can't give you one. What regex can do is check the shape of a token while you scan logs:
^[A-Za-z0-9_-]+.[A-Za-z0-9_-]+.[A-Za-z0-9_-]+$
Three pitfalls sit inside even this simple pattern:
The dots must be escaped. An unescaped . matches any character, so abcXdefYghi sails through a sloppy version of this test.
Anchors matter. Without ^ and $, the pattern matches a valid-looking substring inside a longer piece of junk and reports success.
Greedy quantifiers hide sins. A .* in the middle will swallow the separators you were trying to count.
I test patterns against a handful of deliberately wrong strings, not just the one good string, in a Regex Tester (same site, disclosure below). A pattern that only ever sees valid input has told you nothing.
The two-minute workflow, in order
Read the parse error's line and column. Look left of that point.
Check for trailing commas, single quotes and an undefined that should be null.
Format the payload and read it as a tree, not a string.
For a 401, decode the token locally and compare exp, iss and aud against what the API expects.
Verify the signature on the server before you trust a single claim.
Do it in that order and most "the API is broken" reports resolve into one tired character in the wrong place. It is rarely the API. It is almost never the API.
Disclosure: I build Xplosole, the site behind the three tools linked above. The tutorial works without them — every step has a local-code route, which for real tokens is the route you should take.
URLs verified live — 24 Oct 2026 (all HTTP 200)
https://www.xplosole.com/tools/pdf/pdf-merger
https://www.xplosole.com/tools/image/background-remover
https://www.xplosole.com/tools/image/image-compressor
https://www.xplosole.com/tools/finance/invoice-generator
https://www.xplosole.com/tools/text/json-formatter
https://www.xplosole.com/tools/developer/regex-tester
Top comments (0)