You call JSON.parse(data) and instead of getting a JavaScript object, you get back a string. No error is thrown, but the result is wrong. The cause is almost always double-encoded JSON — JSON.stringify() was called twice on the same data.
What double-encoding looks like
const original = { name: "Alice", age: 30 };
// Normal encoding — produces a JSON string
const encoded = JSON.stringify(original);
// encoded = '{"name":"Alice","age":30}'
// Double encoding — stringify is called again on an already-stringified value
const doubleEncoded = JSON.stringify(encoded);
// doubleEncoded = '"{\\"name\\":\\"Alice\\",\\"age\\":30}"'
// Notice: outer quotes + all inner quotes are escaped with backslash
Now when you parse the double-encoded value:
const result = JSON.parse(doubleEncoded);
typeof result; // "string" ← still a string!
console.log(result); // '{"name":"Alice","age":30}' ← the inner JSON string
// You need to parse once more to get the object
const obj = JSON.parse(result);
typeof obj; // "object" ✅
obj.name; // "Alice" ✅
How to detect it
const result = JSON.parse(data);
if (typeof result === 'string') {
// Double-encoded — need to parse again
console.warn('Data was double-encoded');
const obj = JSON.parse(result);
} else {
// result is the object you expected
}
Where double-encoding typically happens
1. Server-side: JSON.stringify on an already-serialized value
// Node.js / Express — common mistake
app.get('/api/user', (req, res) => {
const user = { name: "Alice" };
const json = JSON.stringify(user); // json is now a string
// ❌ res.json() calls JSON.stringify internally — double encoding
res.json(json);
// ✅ Either use res.json() with the object directly
res.json(user);
// ✅ Or use res.send() with the pre-stringified string
res.set('Content-Type', 'application/json').send(json);
});
2. Storing and retrieving from localStorage
// ❌ Storing a pre-stringified value, then stringifying again
const data = JSON.stringify({ name: "Alice" });
localStorage.setItem('user', JSON.stringify(data)); // double-encoded
const raw = localStorage.getItem('user');
JSON.parse(raw); // → string, not object
// ✅ Correct — stringify the object, not the string
localStorage.setItem('user', JSON.stringify({ name: "Alice" }));
const user = JSON.parse(localStorage.getItem('user')); // → object ✅
3. API gateway or middleware double-serializing
// Some API gateways (AWS API Gateway, certain Express middleware)
// serialize the body automatically. If your handler also stringifies,
// the client receives a double-encoded string.
// ❌ AWS Lambda — body is already serialized by API Gateway
return {
statusCode: 200,
body: JSON.stringify(JSON.stringify({ name: "Alice" })) // ← double
};
// ✅ Correct
return {
statusCode: 200,
body: JSON.stringify({ name: "Alice" }) // ← single
};
4. Passing a JSON string through JSON.stringify in a query parameter
const filter = JSON.stringify({ status: "active" });
// ❌ Stringifying again when building the URL
const url = `/api/users?filter=${JSON.stringify(filter)}`;
// ✅ Correct — encode the already-stringified string for the URL
const url = `/api/users?filter=${encodeURIComponent(filter)}`;
The defensive parse pattern
If you cannot immediately fix the double-encoding at the source, use a safe parser that handles both cases:
function safeParse(value) {
if (typeof value !== 'string') return value; // already an object
try {
const parsed = JSON.parse(value);
// If result is still a string, it was double-encoded
if (typeof parsed === 'string') {
return JSON.parse(parsed);
}
return parsed;
} catch {
throw new SyntaxError('Invalid JSON: ' + value.slice(0, 100));
}
}
The real fix is always to find where the extra JSON.stringify() call is happening and remove it. The defensive parser is a temporary measure — double-encoding in a data pipeline is a bug, not a feature.
Inspect the raw value
Paste the raw value your API returns into the JSON Formatter. If the formatted output is a single quoted string (like "{\\"name\\":\\"Alice\\"}"), the data is double-encoded. If it shows a proper object tree, the data is correctly encoded and the problem is somewhere in your parsing code.