JSON errors are among the most common bugs in web development. Whether you are fetching data from an API, reading from localStorage, or building a configuration system, a single malformed character will throw a SyntaxError and break your application. This guide covers every JSON error you will realistically encounter — what causes it, how to diagnose it, and the exact code to fix it.
70%
JSON errors are syntax issues
#1
cause: missing double quotes
token <
usually means HTML response
100%
preventable with try-catch
What is JSON and Why Is It So Strict?
JSON (JavaScript Object Notation) was designed by Douglas Crockford in the early 2000s as a lightweight data interchange format. Its simplicity is its strength — and also the reason it is so unforgiving. JSON has exactly six value types: string, number, boolean, null, array, and object. Everything else is invalid.
Unlike JavaScript objects, JSON has no notion of undefined, NaN, Infinity, functions, symbols, or comments. This strictness is intentional — JSON must be parseable by any language, not just JavaScript.
JSON vs JavaScript Object: Key Syntax Differences
| Feature | JSON | JavaScript Object |
|---|---|---|
| Key quotes | Required (double quotes only) | Optional for identifiers |
| String quotes | Double quotes only | Single, double, or backtick |
| Trailing commas | NOT allowed | Allowed in modern JS |
| Comments | NOT allowed | // and /* */ allowed |
| undefined values | NOT valid | Valid property value |
| NaN / Infinity | NOT valid | Valid number values |
| Functions | NOT allowed | Allowed as values |
| Date objects | Serialized as string | Native Date object |
Mental model: JSON is a subset of YAML and a strict subset of JavaScript
Overview: The 6 Most Common JSON Errors
Common JSON Error Types
SyntaxError: Unexpected token
Invalid character in JSON — trailing comma, single quote, unescaped character, or comment.
Unexpected token < at position 0
Server returned HTML (error page, login redirect) instead of JSON.
Unexpected end of JSON input
JSON string was truncated — network timeout, size limit, or incomplete generation.
JSON.stringify drops keys
Properties with undefined values are silently omitted by JSON.stringify.
Circular reference error
Object contains a reference to itself — JSON.stringify cannot serialize circular structures.
Deeply nested parse failure
JSON string exceeds parser depth limit, or has non-UTF-8 encoding issues.
How JSON.parse() processes your data
Your Code
Calls JSON.parse(str)
Tokenizer
Reads characters one by one
Parser
Builds object tree
SyntaxError?
Invalid token found
Debug
Read error + position
Fix & Retry
Correct the JSON
Error 1: SyntaxError — Unexpected Token (Bad Syntax)
The most frequent JSON error. The parser hit a character it did not expect. The error message usually includes the position in the string where parsing failed, which is your primary debugging clue.
1a. Trailing Commas
JSON does not allow a comma after the last element in an array or the last property in an object. This is the single most common mistake because JavaScript and most modern languages do allow trailing commas.
Trailing comma in object
// ❌ SyntaxError: Unexpected token }
JSON.parse('{"name": "Alice", "age": 30,}');
// ❌ Also invalid in arrays
JSON.parse('[1, 2, 3,]');// ✅ Remove the trailing comma
JSON.parse('{"name": "Alice", "age": 30}');
// ✅ Arrays too
JSON.parse('[1, 2, 3]');1b. Single Quotes Instead of Double Quotes
JSON requires double quotes for both keys and string values. Single quotes are a syntax error. This often happens when developers copy JavaScript object literal syntax into a JSON context.
Single quotes are not valid JSON
// ❌ Single quotes on keys — SyntaxError
JSON.parse("{'name': 'Alice'}");
// ❌ Mixed quotes
JSON.parse('{"name": 'Alice'}');// ✅ Always use double quotes
JSON.parse('{"name": "Alice"}');
// ✅ In a JS string, escape the inner double quotes
const raw = '{"name": "Alice", "city": "New York"}';
const parsed = JSON.parse(raw);1c. Comments in JSON
JSON has no comment syntax. Any // or /* */ will cause a SyntaxError. If you need comments in config files, use JSON5, HJSON, or YAML instead.
Comments cause SyntaxError
// ❌ Comments are NOT allowed in JSON
{
// User configuration
"theme": "dark",
/* Set to true to enable notifications */
"notifications": true
}// ✅ Remove all comments from JSON
{
"theme": "dark",
"notifications": true
}
// ✅ If you need comments, use JSON5 format
// and the json5 npm package to parse it1d. Unescaped Special Characters
// Inside a JSON string, these characters must be escaped:
{
"path": "C:\\Users\\Alice\\Documents", // \ → \\
"quote": "She said \"hello\"", // " → \"
"newline": "line1\nline2", // newline → \n
"tab": "col1\tcol2", // tab → \t
"emoji": "Works fine: \u2764", // Unicode OK
"null_byte": "NOT OK in plain strings" // \u0000 needs escape
}
// ✅ Use JSON.stringify to generate valid JSON from JS values:
const obj = { message: 'She said "hello"', path: 'C:\Users\Alice' };
const safe = JSON.stringify(obj);
// '{"message":"She said \"hello\"","path":"C:\\Users\\Alice"}'NaN and Infinity are not valid JSON
JSON.stringify(NaN) returns "null", and JSON.stringify(Infinity) also returns "null". This is silent data loss. Always validate numeric values before serializing them if you rely on their exact value.Error 2: "Unexpected token <" — API Returns HTML
This is arguably the most confusing JSON error because it is not really a JSON problem at all — it is a network/server problem. The < character is the first character of an HTML document (<!DOCTYPE or <html). When you see this error, your server returned an HTML page instead of JSON.
Why does the server return HTML instead of JSON?
404 Not Found
Wrong URL — the server returns its 404 HTML error page.
401/403 Unauthorized
Missing or expired auth token — server redirects to login page.
500 Server Error
Backend crash — server returns its default HTML error page.
Wrong endpoint
Hitting the web app URL instead of the API endpoint.
Redirect to login
Session expired, server issues 302 redirect to login HTML.
CDN/WAF block
Cloudflare or WAF returns an HTML block page.
Fix: Check Content-Type before calling .json()
// ❌ Blindly calling .json() — will throw if HTML is returned
const res = await fetch('/api/users');
const data = await res.json(); // 💥 SyntaxError if server returned HTML
// ❌ Also bad: not checking response.ok
const res = await fetch('/api/users');
if (res.ok) {
const data = await res.json(); // Still might return HTML on 4xx
}// ✅ Always check Content-Type and response.ok
async function fetchJSON(url, options) {
const res = await fetch(url, options);
// Check content type FIRST
const ct = res.headers.get('content-type') ?? '';
if (!ct.includes('application/json')) {
const text = await res.text();
throw new Error(
`Expected JSON but got ${ct}. Status: ${res.status}. Body: ${text.slice(0, 200)}`
);
}
if (!res.ok) {
const err = await res.json().catch(() => ({ message: 'Unknown error' }));
throw new Error(`HTTP ${res.status}: ${err.message ?? JSON.stringify(err)}`);
}
return res.json();
}
// Usage
const users = await fetchJSON('/api/users');How to debug this error in DevTools
Error 3: "Unexpected End of JSON Input"
This error means the JSON string ended before the parser finished building the object tree. Imagine receiving {"name": "Alice — the parser opened an object, found the key "name", started reading the string value "Alice", but then hit the end of the string before finding the closing quote, brace, or bracket.
Common causes of truncated JSON
Network timeout or interruption
Most commonThe HTTP connection dropped mid-response. The browser received only part of the response body. Check for network errors in DevTools.
Response size limit hit
A proxy, CDN, or serverless function truncated the response at a size limit (e.g., Lambda 6MB limit). Switch to streaming or pagination.
String was accidentally sliced
Code somewhere called str.slice() or str.substring() on the raw JSON string, cutting it short. Search your codebase for any string manipulation on API responses.
Server-side generation bug
The backend code generating JSON crashed mid-write and sent a partial response without proper error handling. Check server logs.
Empty response body
The server returned HTTP 204 No Content or an empty body, and the code called .json() expecting a payload. Check for empty string before parsing.
async function fetchJSONSafe<T>(url: string, options?: RequestInit): Promise<T> {
let res: Response;
try {
res = await fetch(url, options);
} catch (networkError) {
// DNS failure, timeout, connection refused
throw new Error(`Network error: ${(networkError as Error).message}`);
}
// Handle 204 No Content
if (res.status === 204) return undefined as T;
const text = await res.text();
// Guard against empty body
if (!text || text.trim() === '') {
if (!res.ok) throw new Error(`HTTP ${res.status} with empty body`);
return undefined as T;
}
// Check Content-Type
const ct = res.headers.get('content-type') ?? '';
if (!ct.includes('application/json') && !ct.includes('text/json')) {
throw new Error(
`Expected JSON, got "${ct}". First 200 chars: ${text.slice(0, 200)}`
);
}
let data: T;
try {
data = JSON.parse(text);
} catch (parseError) {
throw new Error(
`JSON parse failed: ${(parseError as Error).message}. ` +
`Raw (first 500 chars): ${text.slice(0, 500)}`
);
}
if (!res.ok) {
const errMsg = (data as any)?.message ?? (data as any)?.error ?? JSON.stringify(data);
throw new Error(`HTTP ${res.status}: ${errMsg}`);
}
return data;
}Error 4: JSON.stringify Silently Drops undefined
This is a subtle bug that does not throw an error — it causes silent data loss. When JSON.stringify encounters an object property whose value is undefined, it simply skips the key. The resulting JSON string will not contain that key at all. If you later parse it, the key will be missing.
JSON.stringify behavior with undefined, NaN, Infinity
// ❌ These values are silently dropped or converted
const user = {
id: 1,
name: 'Alice',
avatar: undefined, // ← dropped entirely
score: NaN, // ← becomes null
ratio: Infinity, // ← becomes null
};
const json = JSON.stringify(user);
// '{"id":1,"name":"Alice","score":null,"ratio":null}'
// Note: "avatar" key is completely missing!
const parsed = JSON.parse(json);
console.log('avatar' in parsed); // false — data lost!// ✅ Option 1: Use null explicitly for "no value"
const user = {
id: 1,
name: 'Alice',
avatar: null, // ← null IS valid JSON
};
JSON.stringify(user);
// '{"id":1,"name":"Alice","avatar":null}'
// ✅ Option 2: Use a replacer to convert undefined → null
function undefinedToNull(_key: string, value: unknown) {
return value === undefined ? null : value;
}
JSON.stringify(user, undefinedToNull);
// ✅ Option 3: Use a custom serializer for NaN/Infinity
function numberReplacer(_key: string, value: unknown) {
if (typeof value === 'number') {
if (Number.isNaN(value)) return 'NaN';
if (!Number.isFinite(value)) return value > 0 ? 'Infinity' : '-Infinity';
}
return value;
}
JSON.stringify({ score: NaN, ratio: Infinity }, numberReplacer);
// '{"score":"NaN","ratio":"Infinity"}' — strings, but at least not lostJSON.stringify array behavior with undefined
undefined is not dropped — it is converted to null:JSON.stringify([1, undefined, 3]) produces [1,null,3]. This is different from object behavior where the key is omitted entirely. Both cases are silent and can cause confusing bugs.Error 5: Circular Reference — JSON.stringify Fails
If an object contains a reference back to itself (directly or through a chain), JSON.stringify will throw a TypeError: Converting circular structure to JSON. This often happens with DOM nodes, Express req/res objects, or complex graph-like data structures.
// ❌ This throws TypeError
const obj: any = { name: 'Alice' };
obj.self = obj; // circular reference!
JSON.stringify(obj); // TypeError: Converting circular structure to JSON
// ✅ Option 1: Use a WeakSet to detect cycles
function safeStringify(obj: unknown, indent?: number): string {
const seen = new WeakSet();
return JSON.stringify(obj, (_key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
}, indent);
}
// ✅ Option 2: Use the 'flatted' or 'json-stringify-safe' npm package
import stringify from 'json-stringify-safe';
const safe = stringify(circularObj, null, 2);
// ✅ Option 3: For logging, use util.inspect (Node.js)
import util from 'util';
console.log(util.inspect(circularObj, { depth: 4, circular: true }));Try-Catch Patterns: Never Let JSON.parse Crash Your App
JSON.parse() throws a synchronous SyntaxError on invalid input. If you do not catch it, the error will propagate up and potentially crash your application or render a broken UI. Wrapping JSON.parse in try-catch is not optional in production code — it is mandatory.
The JSON debugging workflow
Read the exact error message
Step 1The error message includes the position: "at position 42". That position points to the character that broke the parser. Count from the start of the string.
Log the raw string
Step 2Before parsing, log the raw string (or the first 500 characters). This reveals whether the server sent HTML, an empty string, or malformed JSON.
Check Content-Type header
Step 3If the Content-Type is text/html instead of application/json, the server returned an error page. Fix the server issue, not the JavaScript.
Use a JSON validator
Step 4Paste the raw string into a JSON validator. It will highlight exactly which character is invalid and why. Our JSON Fixer can often auto-correct common errors.
Fix the source, not the symptom
Step 5If the server is sending bad JSON, fix the server. If you are hardcoding JSON strings, use JSON.stringify() to generate them instead of writing them by hand.
// ── 1. Safe parse with fallback ──────────────────────────────
export function parseJSON<T>(raw: string, fallback: T): T {
try {
return JSON.parse(raw) as T;
} catch {
return fallback;
}
}
// ── 2. Safe parse that returns Result type ────────────────────
type ParseResult<T> =
| { ok: true; data: T }
| { ok: false; error: string; raw: string };
export function parseJSONResult<T>(raw: string): ParseResult<T> {
try {
return { ok: true, data: JSON.parse(raw) as T };
} catch (e) {
return {
ok: false,
error: (e as SyntaxError).message,
raw: raw.slice(0, 200),
};
}
}
// Usage:
const result = parseJSONResult<User>(apiResponse);
if (!result.ok) {
console.error('Parse failed:', result.error, 'Raw:', result.raw);
showErrorToUser('Failed to load data. Please refresh.');
} else {
setUser(result.data);
}
// ── 3. localStorage safe helpers ─────────────────────────────
export function getLocalStorageJSON<T>(key: string, fallback: T): T {
try {
const raw = localStorage.getItem(key);
if (raw === null) return fallback;
return JSON.parse(raw) as T;
} catch {
// localStorage had corrupted data — clear and return fallback
localStorage.removeItem(key);
return fallback;
}
}
export function setLocalStorageJSON(key: string, value: unknown): boolean {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch {
// Quota exceeded or value is not serializable
return false;
}
}Real-World Examples: JSON Errors in Common Scenarios
React: Fetch and Parse API Response
import { useState, useEffect } from 'react';
interface ApiResponse<T> {
data: T | null;
loading: boolean;
error: string | null;
}
export function useAPI<T>(url: string): ApiResponse<T> {
const [state, setState] = useState<ApiResponse<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let cancelled = false;
async function load() {
setState(s => ({ ...s, loading: true, error: null }));
try {
const res = await fetch(url);
// Check content type before parsing
const ct = res.headers.get('content-type') ?? '';
if (!ct.includes('application/json')) {
const text = await res.text();
throw new Error(
`Server returned non-JSON (${ct}). ` +
`Status: ${res.status}. Body: ${text.slice(0, 100)}`
);
}
const data: T = await res.json();
if (!cancelled) {
setState({ data, loading: false, error: null });
}
} catch (e) {
if (!cancelled) {
setState({ data: null, loading: false, error: (e as Error).message });
}
}
}
load();
return () => { cancelled = true; };
}, [url]);
return state;
}
// Component usage
function UserProfile({ userId }: { userId: string }) {
const { data: user, loading, error } = useAPI<User>(`/api/users/${userId}`);
if (loading) return <Spinner />;
if (error) return <ErrorBanner message={error} />;
return <div>{user?.name}</div>;
}Node.js: Parse Request Body
import express from 'express';
import { z } from 'zod'; // zod for schema validation
const app = express();
app.use(express.json({
// Handle JSON parse errors at the middleware level
strict: true,
limit: '1mb',
}));
// Handle JSON parse errors from express.json() middleware
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
if (err.type === 'entity.parse.failed') {
return res.status(400).json({
error: 'Invalid JSON in request body',
detail: err.message,
});
}
next(err);
});
// Define schema with zod
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(0).max(150),
});
app.post('/api/users', (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: 'Validation failed',
details: result.error.format(),
});
}
const { name, email, age } = result.data;
// ... create user
res.status(201).json({ id: 'new-id', name, email, age });
});Prevention: How to Write JSON That Never Breaks
Golden rule: never handwrite JSON strings in production code
JSON.stringify() to generate JSON from JavaScript objects. It handles all escaping, quoting, and serialization correctly. Only handwrite JSON in configuration files, and always validate them with a JSON validator.JSON error prevention checklist
Never handwrite JSON strings
Use JSON.stringify() to generate JSON. Manual JSON is error-prone.
Always wrap JSON.parse in try-catch
Treat all external JSON as untrusted. Never assume it is valid.
Check Content-Type before parsing
Verify the server returned application/json before calling .json().
Validate schemas with Zod or Yup
Even valid JSON may have wrong types. Use runtime schema validation.
Use TypeScript for JSON types
Define interfaces for your API responses to catch type mismatches.
Lint JSON files in CI
Use jq or a JSON lint step to catch config file errors before deployment.
# Validate a JSON file with jq
jq . config.json
# Pretty-print a JSON file
jq . raw.json | less
# Query nested data
curl -s https://api.example.com/users | jq '.data[0].name'
# Check if an API returns JSON (curl)
curl -I https://api.example.com/users
# Look for: Content-Type: application/json
# Validate JSON string in Node.js
node -e "JSON.parse(require('fs').readFileSync('data.json', 'utf8')); console.log('Valid!')"
# Fix common JSON issues with python
python3 -c "import json,sys; json.dump(json.load(sys.stdin), sys.stdout, indent=2)"
# Install jsonlint globally for nicer errors
npm i -g jsonlint
jsonlint config.jsonPaste any JSON and instantly see which line has the error, with a clear explanation of what is wrong.
Validate JSON →Auto-fix trailing commas, single quotes, comments, and other common JSON errors in one click.
Fix My JSON →Frequently Asked Questions
Share this article with Your Friends, Collegue and Team mates
Stay Updated
Get the latest tool updates, new features, and developer tips delivered to your inbox.
Occasional useful updates only. Unsubscribe in one click — we never sell your email.
Feedback for Fix JSON Errors Guide
Tell us what's working, what's broken, or what you wish we built next — it directly shapes our roadmap.
Good feedback is gold — a rough edge you hit today could be smoother for everyone tomorrow.
- Feature ideas often jump the queue when lots of you ask.
- Bug reports with steps get fixed faster — paste URLs or examples if you can.
- Name and email are optional; we won't use them for anything except replying if needed.