Why it happens
Firefox reports the line and column of the first character it could not use, and that position sorts the problem:
| Position | Character found there | What it means |
|---|---|---|
| line 1 column 1 | < |
an HTML page or a PHP warning instead of JSON |
| line 1 column 1 | a letter other than t, f or n | a plain-text body, or the value undefined |
| line 1 column 2 | o |
the text [object Object] |
| anywhere else | ', T, N and similar |
a syntax mistake inside otherwise valid JSON |
Column 1 causes, most common first:
1. PHP printed a warning before the JSON. With display_errors on, warnings go into the response as HTML. In the sample, the JSON is intact on the last line, behind a <br />. WordPress plugins are the classic source.
2. The server sent an HTML page: a 404, a 500 error page or a login form. Chrome calls this Unexpected token ‘<’, “<!DOCTYPE “… is not valid JSON; that page covers each case.
3. The body is plain text. Express’s res.sendStatus(500) sends Internal Server Error, and many rate limiters send Too many requests, please try again later.
4. The value was never a string. JSON.parse(undefined) fails at column 1; an object becomes the text [object Object] and fails at column 2.
Other columns point inside the document: {"name": 'Lena'} fails at column 10, on the single quote. An empty body gives unexpected end of data at line 1 column 1 instead, covered under Unexpected end of JSON input.
How to fix JSON.parse: unexpected character
Read the raw response in Firefox
Open the Network panel (Ctrl + Shift + E, or Cmd + Option + E on macOS), reload and select the failing request. The Headers tab shows the status and Content-Type; the Response tab shows the body your code received. Paste it into the JSON validator to highlight the first bad character.
fetch: keep the body when parsing fails
A failed res.json() consumes the body, so the text that broke it is lost. Read it as text and put it in the error:
async function readJson(res) {
const text = await res.text();
try {
return JSON.parse(text);
} catch {
throw new Error(`${res.status} ${res.url} returned non-JSON: ${text.slice(0, 200)}`);
}
}
const form = document.querySelector('form');
const result = await readJson(await fetch('/api/signup.php', { method: 'POST', body: new FormData(form) }));
PHP: log warnings instead of printing them
Fix the warning, and keep future ones out of the response:
<?php
ini_set('display_errors', '0');
ini_set('log_errors', '1');
header('Content-Type: application/json');
$email = $_POST['email'] ?? null; // no "Undefined array key" warning
if ($email === null) {
http_response_code(422);
echo json_encode(['error' => 'email is required']);
exit;
}
echo json_encode(['ok' => true, 'id' => 381]);
In WordPress, set these in wp-config.php so notices go to wp-content/debug.log instead of into admin-ajax.php responses:
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
Someone else’s app
If a webmail client or self-hosted dashboard shows it, the app’s server returned an error or login page. Reload and sign in again; if you run the service, check that its backend is up.
Same error in other browsers and languages
- Chrome, Edge, Node.js:
Unexpected token '<', "<!DOCTYPE "... is not valid JSONfor an HTML body. - Safari: JSON Parse error: Unrecognized token ‘<’
- Python:
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)for the same HTML body.
More parse errors and their causes: common JSON errors.