JSON.parse() throws a SyntaxError synchronously when given invalid input. If you do not catch it, the error propagates up the call stack and can crash your application or produce a blank UI. This article covers the most practical patterns for handling this error correctly.
The basic try/catch
try {
const data = JSON.parse(jsonString);
// use data safely here
} catch (error) {
console.error('JSON parse failed:', error.message);
// handle the error: show a message, use a default, etc.
}
This is the minimum. error.message will contain the parser's error description, which includes the character position of the problem — for example, Unexpected token '}' at position 42.
A reusable safe-parse function
If you parse JSON in many places, a helper function prevents repetition:
function tryParseJSON(str, fallback = null) {
try {
return JSON.parse(str);
} catch {
return fallback;
}
}
// Usage
const config = tryParseJSON(localStorage.getItem('config'), {});
const items = tryParseJSON(response.body, []);
The fallback parameter lets each call site specify a sensible default, making the behavior explicit rather than always returning null.
When to throw vs when to recover
Whether to silently recover or re-throw depends on where the invalid JSON comes from:
Recover silently for optional user preferences
If the JSON comes from localStorage or user input and a default is acceptable, return the fallback and log a warning — do not crash:
const prefs = tryParseJSON(localStorage.getItem('prefs'), defaultPrefs);
Throw with a clear message for external API responses
If a server is supposed to return valid JSON and it does not, that is a real error that should be surfaced:
async function fetchData(url) {
const res = await fetch(url);
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch (e) {
throw new Error(
`API at ${url} returned invalid JSON.\n` +
`Status: ${res.status}\n` +
`Parse error: ${e.message}\n` +
`Response preview: ${text.slice(0, 200)}`
);
}
return data;
}
Including the raw response preview in the error makes debugging much faster — you can see immediately whether the server returned an HTML error page, a truncated response, or something else.
Getting line and column from the error
The built-in SyntaxError from JSON.parse includes the character offset but not a line number. To calculate the line number from the offset:
function parseJSONWithPosition(str) {
try {
return { value: JSON.parse(str), error: null };
} catch (e) {
// Extract position from the error message (not 100% portable across engines)
const match = e.message.match(/position (\d+)/);
const pos = match ? parseInt(match[1], 10) : -1;
if (pos >= 0) {
const before = str.slice(0, pos);
const line = before.split('\n').length;
const col = before.length - before.lastIndexOf('\n');
return { value: null, error: `${e.message} (line ${line}, col ${col})` };
}
return { value: null, error: e.message };
}
}
For production tooling, using the JSON Validator is more reliable — it shows the exact line and column with syntax highlighting in the browser.
TypeScript: narrowing the return type
function tryParseJSON<T = unknown>(str: string): T | null {
try {
return JSON.parse(str) as T;
} catch {
return null;
}
}
// With a type assertion:
const user = tryParseJSON<{ name: string; age: number }>(raw);
Note that JSON.parse has no runtime type checking — even with a TypeScript generic, you should validate the shape of the parsed object before using it in production. Libraries like Zod or Valibot handle this cleanly.