{"id":146,"date":"2026-05-31T13:10:29","date_gmt":"2026-05-31T13:10:29","guid":{"rendered":"https:\/\/json-beautifier.org\/blog\/?p=146"},"modified":"2026-05-31T14:58:32","modified_gmt":"2026-05-31T14:58:32","slug":"json-parse-unexpected-token-error","status":"publish","type":"post","link":"https:\/\/json-beautifier.org\/blog\/json-parse-unexpected-token-error\/","title":{"rendered":"Why Your JSON.parse() Keeps Failing: The &#8220;Unexpected Token&#8221; Error Explained"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">You&#8217;ve written the code a hundred times. const data = JSON.parse(response). It always works.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Except today. Today, your console is screaming:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SyntaxError: Unexpected token &lt; in JSON at position 0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">And you&#8217;re staring at the screen wondering what changed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s the thing &#8211; JSON.parse() isn&#8217;t actually broken, and your JSON probably isn&#8217;t broken in the way you think. The &#8220;Unexpected Token&#8221; error is almost always one of seven very specific situations. Once you know which one is biting you, the fix takes about ten seconds.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let&#8217;s walk through all seven, with the actual error messages you&#8217;ll see and exactly what to do.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>First: What This Error Message Actually Says<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The error follows a predictable format:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SyntaxError: Unexpected token X in JSON at position N<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>X<\/strong> is the specific character the parser choked on<\/li>\n\n\n\n<li><strong>N<\/strong> is the position in the string where that character appears<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Read this literally. If it says Unexpected token &lt;, the parser found a &lt; symbol where it expected JSON. That&#8217;s a real clue \u2014 it usually means the server returned HTML instead of JSON. Each character has a different story, and we&#8217;ll cover the common ones below.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Cause #1: The Server Returned HTML, Not JSON<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the #1 reason for JSON.parse() failures in production. The error usually looks like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SyntaxError: Unexpected token &lt; in JSON at position 0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That &lt; is the opening of &lt;!DOCTYPE html> or &lt;html>. Your API returned an HTML error page &#8211; usually a 404, 500, or a login redirect &#8211; and your code tried to parse it as JSON.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How to debug:<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before parsing, log the raw response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>javascript\n\nconst response = await fetch('\/api\/users');\nconst text = await response.text();\nconsole.log('Raw response:', text);\nconsole.log('Status:', response.status);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If you see HTML in the console, your API isn&#8217;t returning JSON. The cause might be:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A 404 (wrong URL)<\/li>\n\n\n\n<li>A 500 (server crashed)<\/li>\n\n\n\n<li>A redirect to a login page (auth expired)<\/li>\n\n\n\n<li>A misconfigured proxy or CDN returning its own error page<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The fix:<\/strong> Always check response.ok or the status code before parsing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>javascript\n\nconst response = await fetch('\/api\/users');\nif (!response.ok) {\n\u00a0\u00a0throw new Error(`API failed with status ${response.status}`);\n}\nconst data = await response.json();<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Cause #2: Empty Response &#8211; &#8220;Unexpected End of JSON Input&#8221;<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The error message here is slightly different:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SyntaxError: Unexpected end of JSON input<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This means JSON.parse() reached the end of the string before finding complete, valid JSON. The two most common reasons:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>1. The server returned an empty body.<\/strong> Some APIs return 204 No Content on success, or an empty 200 on certain operations. Your code then tries to parse &#8220;&#8221;, which fails immediately.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>2. The response got truncated mid-transfer.<\/strong> Network hiccup, proxy timeout, or a server that closed the connection early.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The fix:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>javascript\n\nconst text = await response.text();\nif (!text) {\n\u00a0\u00a0return null; \/\/ or {} or whatever default makes sense\n}\nconst data = JSON.parse(text);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If truncation is the issue, log response.headers.get(&#8216;content-length&#8217;) and compare it to text.length. If they don&#8217;t match, something between the server and your code is chopping the response.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Cause #3: You&#8217;re Parsing Something That&#8217;s Already an Object<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This one is sneaky because the code <em>looks<\/em> right.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>javascript\n\nconst response = await axios.get('\/api\/users');\nconst data = JSON.parse(response); \/\/ \u274c Will fail<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Axios (and many other HTTP clients) automatically parse JSON responses for you. So response is already a JavaScript object &#8211; and JSON.parse() expects a string. When it tries to parse [object Object], it dies.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The error usually looks like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SyntaxError: Unexpected token o in JSON at position 1<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That o is the second character of [object Object].<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The fix:<\/strong> Don&#8217;t double-parse.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>javascript\n\nconst response = await axios.get('\/api\/users');\nconst data = response.data; \/\/ Already parsed by axios<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The general rule: if your HTTP library exposes a response. data or returns the parsed body directly; never call JSON.parse() on it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Cause #4: JSONP Response Wrapped in a Function Call<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you&#8217;re working with older APIs (or some legacy financial\/government endpoints), you might get a response that looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>javascript\n\ncallback({\"name\": \"Alice\", \"age\": 30});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is JSONP &#8211; a workaround from before CORS existed. The data is JSON wrapped inside a function call.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">JSON.parse() chokes on it because of the leading callback(:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SyntaxError: Unexpected token c in JSON at position 0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The fix:<\/strong> Either configure your client to actually use JSONP, or strip the wrapper manually:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>javascript\n\nconst text = await response.text();\nconst jsonPart = text.replace(\/^&#91;^(]+\\(\/, '').replace(\/\\)\\s*;?\\s*$\/, '');\nconst data = JSON.parse(jsonPart);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That regex removes everything up to and including the opening (, and the closing ) and optional semicolon. Not elegant &#8211; but it works for nearly any JSONP response in the wild.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Cause #5: The JSON Itself Is Malformed<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sometimes the server <em>does<\/em> return JSON. It&#8217;s just broken JSON.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Trailing commas, single quotes, unescaped characters, missing brackets &#8211; any of these will trip up JSON.parse(). The error message varies depending on what&#8217;s wrong:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Unexpected token } in JSON at position 47 &#8211; trailing comma before the }<\/li>\n\n\n\n<li>Unexpected token &#8216; in JSON at position 5 &#8211; single quotes used instead of double<\/li>\n\n\n\n<li>Unexpected token \\n in JSON at position 23 &#8211; unescaped newline inside a string<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The fix:<\/strong> Paste the raw response into a<a href=\"https:\/\/json-beautifier.org\/\"> JSON validator<\/a>. It will highlight the exact issue. Then either fix the source that&#8217;s generating the JSON, or &#8211; if you don&#8217;t control the source &#8211; clean it up before parsing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For a full breakdown of malformed-JSON patterns, we covered the 10 most common JSON errors in a separate post. Worth bookmarking.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Cause #6: BOM or Hidden Characters at the Start<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Some servers (especially older PHP, classic ASP, or anything writing UTF-8 files in Windows) prepend a Byte Order Mark to the response. It&#8217;s an invisible character that looks like nothing in your console but absolutely breaks JSON.parse().<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The error is usually:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SyntaxError: Unexpected token\u00a0 in JSON at position 0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That blank space after &#8220;token&#8221; is actually the BOM character &#8211; invisible, but there.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The fix:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>javascript\n\nconst text = await response.text();\nconst clean = text.replace(\/^\\uFEFF\/, ''); \/\/ Strip BOM if present\nconst data = JSON.parse(clean);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The same trick saves you from invisible whitespace pasted from PDFs, Slack messages, or Word documents. If you&#8217;re parsing JSON that came from a copy-paste, always normalize it first.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Cause #7: Double-Stringified JSON<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This one drives developers crazy because the JSON <em>looks<\/em> valid:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>javascript\n\nconst response = \"\\\"{\\\\\\\"name\\\\\\\":\\\\\\\"Alice\\\\\\\"}\\\"\";\nconst data = JSON.parse(response);\nconsole.log(data); \/\/ \"{\\\"name\\\":\\\"Alice\\\"}\" \u2014 still a string!<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">What happened? The data was stringified twice somewhere upstream. Maybe someone did JSON.stringify(JSON.stringify(obj)). Maybe a queue serializer wrapped an already-JSON payload. Either way, one parse gets you a string, not an object.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The fix:<\/strong> Parse twice.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>javascript\n\nconst data = JSON.parse(JSON.parse(response));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">But really, the root fix is to find where the double-stringification is happening upstream and remove it. Double-parsing is a workaround, not a solution.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How to spot this:<\/strong> if JSON.parse() succeeds but typeof result === &#8216;string&#8217;, you&#8217;ve got double-stringified JSON.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>The 30-Second Debug Checklist<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When the error hits and you don&#8217;t know where to start, run through this in order:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Log the raw response<\/strong> before parsing. Is it actually JSON? Or is it HTML, empty, or wrapped in something?<\/li>\n\n\n\n<li><strong>Check the response status.<\/strong> A 4xx or 5xx almost always means the body isn&#8217;t JSON.<\/li>\n\n\n\n<li><strong>Check what you&#8217;re parsing.<\/strong> Is it already an object? If yes, you don&#8217;t need JSON.parse().<\/li>\n\n\n\n<li><strong>Count the characters.<\/strong> If the error says &#8220;position 23&#8221;, look at character 23. Literally count to it.<\/li>\n\n\n\n<li><strong>Run it through a validator.<\/strong> Paste the raw response into a<a href=\"https:\/\/json-beautifier.org\/\"> JSON validator<\/a> &#8211; it&#8217;ll point at the exact broken character.<\/li>\n\n\n\n<li><strong>Check for invisible characters.<\/strong> If position 0 looks fine to your eyes, it&#8217;s probably a BOM or hidden whitespace.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">In our experience, causes #1 (HTML response), #3 (already parsed), and #5 (malformed JSON) cover about 80% of real-world JSON.parse() failures. Start there.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>A Better Pattern: Wrap Your Parsing<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once you&#8217;ve debugged this enough times, you stop wanting to write the same try\/catch over and over. Build a safe parser once and reuse it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>javascript\n\nfunction safeJsonParse(text, fallback = null) {\n\u00a0\u00a0if (!text || typeof text !== 'string') return fallback;\n\u00a0\u00a0\/\/ Strip BOM if present\n\u00a0\u00a0const clean = text.replace(\/^\\uFEFF\/, '').trim();\n\u00a0\u00a0if (!clean) return fallback;\n\u00a0\u00a0try {\n\u00a0\u00a0\u00a0\u00a0return JSON.parse(clean);\n\u00a0\u00a0} catch (err) {\n\u00a0\u00a0\u00a0\u00a0console.warn('JSON parse failed:', err.message);\n\u00a0\u00a0\u00a0\u00a0console.warn('First 100 chars:', clean.substring(0, 100));\n\u00a0\u00a0\u00a0\u00a0return fallback;\n\u00a0\u00a0}\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This handles empty strings, BOMs, and bad inputs gracefully &#8211; and logs enough context that future-you can actually debug what went wrong.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>The Honest Truth<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">JSON.parse() is one of the most reliable functions in JavaScript. When it throws, it&#8217;s almost never the parser&#8217;s fault &#8211; it&#8217;s the data you&#8217;re feeding it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fastest path to fixing any &#8220;Unexpected token&#8221; error is to <strong>look at the raw input first<\/strong>, before you touch the code. Nine times out of ten, the answer is staring back at you the moment you log the response.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And if you want to stop ever guessing at JSON errors again,<a href=\"https:\/\/json-beautifier.org\/\"> json-beautifier.org<\/a> runs entirely in your browser &#8211; paste the raw API response, see exactly what&#8217;s wrong, fix it, and move on. If you&#8217;re new to the world of JSON tools and not sure which one does what, we broke down the differences between beautifiers, validators, formatters, and minifiers in another post.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now go fix that error. You&#8217;ve got this.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>You&#8217;ve written the code a hundred times. const data = JSON.parse(response). It always works. Except today. Today, your console is [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":148,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[],"class_list":["post-146","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-json-beautifier"],"_links":{"self":[{"href":"https:\/\/json-beautifier.org\/blog\/wp-json\/wp\/v2\/posts\/146","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/json-beautifier.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/json-beautifier.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/json-beautifier.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/json-beautifier.org\/blog\/wp-json\/wp\/v2\/comments?post=146"}],"version-history":[{"count":1,"href":"https:\/\/json-beautifier.org\/blog\/wp-json\/wp\/v2\/posts\/146\/revisions"}],"predecessor-version":[{"id":147,"href":"https:\/\/json-beautifier.org\/blog\/wp-json\/wp\/v2\/posts\/146\/revisions\/147"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/json-beautifier.org\/blog\/wp-json\/wp\/v2\/media\/148"}],"wp:attachment":[{"href":"https:\/\/json-beautifier.org\/blog\/wp-json\/wp\/v2\/media?parent=146"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/json-beautifier.org\/blog\/wp-json\/wp\/v2\/categories?post=146"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/json-beautifier.org\/blog\/wp-json\/wp\/v2\/tags?post=146"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}