I’ve run into this problem more times than I’d like to admit: I’m happily building an AJAX feature, jQuery is supposed to parse JSON, and suddenly it blows up with a mysterious parsererror
.
After digging around, I realized the culprit wasn’t jQuery at all it was PHP quietly printing HTML error pages (often from Xdebug or display_errors=On
) before my JSON. Let me walk you through what’s happening, how I fixed it, and a few extra tricks I added along the way.
The Problem Version
Here’s where I started:
function getMyCharities(callback) {
$.post("my_file.php", function (data) {
callback(data); // data may contain HTML before JSON
});
}
The issue?
- jQuery (or
JSON.parse
) expects pure JSON. - PHP errors (warnings, notices, fatals) often come out as an HTML table with
<xdebug-error>
markup. - That HTML gets prepended to my JSON.
- Result: response begins with
<
instead of{
or[
, and parsing fails withparsererror
.
The Server Side Fix Always Return Clean JSON
The first step was fixing things at the source. I made sure my PHP scripts never leak HTML errors and always return well-formed JSON.
Here’s the pattern I use:
<?php
// my_file.php
// Never print PHP errors to the response; log them instead.
ini_set('display_errors', '0');
ini_set('log_errors', '1');
error_reporting(E_ALL);
// Convert errors and exceptions to JSON
set_exception_handler(function (Throwable $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'ok' => false,
'error' => [
'type' => get_class($e),
'message' => $e->getMessage(),
],
]);
exit;
});
set_error_handler(function ($severity, $message, $file, $line) {
throw new ErrorException($message, 0, $severity, $file, $line);
});
// Prevent accidental echo/whitespace
ob_start();
header('Content-Type: application/json; charset=utf-8');
try {
$charities = [
['id' => 1, 'name' => 'Doctors Without Borders'],
['id' => 2, 'name' => 'Charity: Water'],
];
ob_clean(); // clear buffer
echo json_encode(['ok' => true, 'data' => $charities], JSON_UNESCAPED_UNICODE);
exit;
} catch (Throwable $e) {
throw $e; // delegate to handler
}
Now, no matter what happens, the response is valid JSON. If there’s an error, I return an error object instead of a half-broken HTML page.
The Client Side Fix Handle JSON Safely
On the client, I switched to $.ajax
with dataType: 'json'
and added guards.
function getMyCharities() {
return $.ajax({
url: 'my_file.php',
method: 'POST',
dataType: 'json', // jQuery will auto-parse JSON
timeout: 7000
})
.then(function (resp) {
if (!resp || resp.ok !== true || !Array.isArray(resp.data)) {
return $.Deferred().reject({ kind: 'bad-shape', resp: resp }).promise();
}
return resp.data;
})
.fail(function (jqXHR, textStatus) {
if (textStatus === 'parsererror') {
console.error('Response was not valid JSON. Check PHP output/Xdebug.');
} else if (textStatus === 'timeout') {
console.error('Request timed out.');
} else {
console.error('AJAX failed:', textStatus, jqXHR.status);
}
});
}
Now my frontend can tell whether the response was good data, bad JSON, or a network/server error.
Practice Enhancement
Once I had the basics working, I added some quality-of-life features:
Retry with Backoff
function fetchWithRetry({ maxTries = 3, baseDelay = 400 } = {}) {
let attempt = 0;
function run() {
attempt++;
return getMyCharities()
.then(data => data)
.catch(err => {
if (attempt >= maxTries) throw err;
const wait = baseDelay * Math.pow(2, attempt - 1);
return new Promise(res => setTimeout(res, wait)).then(run);
});
}
return run();
}
Cancel an In Flight Request
const req = getMyCharities();
$('#cancelBtn').on('click', function () {
req.abort(); // jqXHR supports abort
});
User Friendly Error Toast
function showErrorToast(err) {
const msg = (err && err.resp && err.resp.error && err.resp.error.message)
? err.resp.error.message
: 'Something went wrong. Please try again.';
alert(msg);
}
Salvaging Mixed HTML + JSON
If you can’t fix PHP right away, you can extract the first JSON block from the response. It’s fragile, but it works as a stopgap:
function extractFirstJson(text) {
const match = text.match(/(\{[\s\S]*\}|\[[\s\S]*\])/);
if (!match) throw new Error('No JSON found in response');
return JSON.parse(match[1]);
}
$.ajax({
url: 'my_file.php',
method: 'POST',
dataType: 'text'
})
.then(function (raw) {
const json = extractFirstJson(raw);
if (!json.ok) throw { resp: json };
return json.data;
})
.then(renderCharities)
.catch(showErrorToast);
Fix the server. This hack is just duct tape.
Final Thought
The real lesson I learned was this don’t trust PHP to “just print JSON.” I had to build a disciplined pattern where the server always outputs proper JSON and never leaks HTML errors, while the client explicitly requests dataType:'json'
and guards against parser errors, timeouts, and schema mismatches.