JSON.parse in JavaScript: Safe Parsing, Errors, and Fallbacks
JSON.parse in JavaScript is a syntax boundary, not a trust boundary. It can tell you whether a string contains valid JSON text. It cannot tell you whether the parsed value is the user settings, API payload, cache entry, or feature flag shape your code wanted. That second job belongs to validation.
This distinction matters any time the string did not come directly from code you just wrote: localStorage, fetch, user input, query parameters, copied config, test fixtures, or old cached data.
Contents
- JSON.parse needs valid JSON text
- Malformed JSON throws SyntaxError
- Wrap parsing in try/catch
- Use fallback values deliberately
- Parsing is not validation
- Where this fits with JSON, localStorage, and fetch
- Key Takeaways
JSON.parse needs valid JSON text
JSON.parse converts JSON text into a JavaScript value:
const user = JSON.parse('{"name":"Ada","active":true}');
console.log(user.name); // "Ada"
console.log(user.active); // true
The input is a string, but not just any string. It has to follow JSON syntax.
These are valid JSON strings:
JSON.parse('{"theme":"dark","fontSize":16}');
JSON.parse('["drafts","settings","history"]');
JSON.parse('"hello"');
JSON.parse("false");
JSON.parse("0");
JSON.parse("null");
That last group is easy to forget. A JSON document does not have to be an object. Strings, numbers, booleans, arrays, objects, and null can all be valid JSON values.
These are not valid JSON strings:
JSON.parse("{ name: 'Ada' }");
JSON.parse('{"name":"Ada",}');
JSON.parse("undefined");
JSON.parse("// nope");
JSON looks like JavaScript object literal syntax, but it is stricter:
- Object keys must use double quotes.
- String values must use double quotes.
- Trailing commas are not allowed.
- Comments are not allowed.
undefined, functions, symbols, and class instances are not JSON values.
In normal application code, treat JSON.parse as a parser for strings. JavaScript can coerce some non-string inputs before parsing, which leads to strange edge cases. That is not a good API contract. If your helper accepts unknown input, check that it is a string first.
Malformed JSON throws SyntaxError
When JSON text is malformed, JSON.parse throws a SyntaxError.
const raw = '{"theme":"dark",}';
const settings = JSON.parse(raw);
console.log(settings.theme);
The parse line throws before settings is assigned. The final console.log never runs.
Different JavaScript engines phrase the message differently, but the category is the important part:
try {
JSON.parse('{"theme":"dark",}');
} catch (error) {
console.log(error instanceof SyntaxError); // true
}
That loud failure is useful. Invalid data should not quietly become a mostly empty object, a half-parsed array, or whatever shape lets the next bug hide for ten minutes.
Wrap parsing in try/catch
If an invalid string is a normal possibility, parse at the boundary and catch the parse failure there.
function parseJsonOrFallback(raw, fallback) {
if (typeof raw !== "string") {
return fallback;
}
try {
return JSON.parse(raw);
} catch {
return fallback;
}
}
const raw = '{"theme":"dark"}';
const settings = parseJsonOrFallback(raw, {});
console.log(settings); // { theme: "dark" }
The important part is the shape of the helper:
- The success path returns the parsed value.
- The failure path returns the fallback.
- The fallback is only used when the input is not a string or parsing throws.
Keep the try block focused. If you put a lot of extra code inside it, the catch block can hide bugs that were not parse errors.
Prefer this:
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
parsed = {};
}
const theme = parsed.theme;
Over this:
try {
const parsed = JSON.parse(raw);
const theme = parsed.preferences.theme;
applyTheme(theme);
} catch {
applyTheme("light");
}
The second version catches bad JSON, but it also catches TypeError from parsed.preferences.theme and any error thrown by applyTheme. That makes debugging less honest. Parse first, then handle the parsed value with ordinary code.
Use fallback values deliberately
A fallback is a real value the rest of the program can work with.
const defaultFlags = {
newDashboard: false,
compactNav: false,
};
const flags = parseJsonOrFallback(rawFlags, defaultFlags);
Choose a fallback that matches what callers expect. If the caller expects an array, use []. If it expects an object with known keys, use an object with those keys. If absence is meaningful, use null and handle it explicitly.
Do not write fallback logic that treats valid falsy JSON values as failures.
function parseJsonOrFallbackWrong(raw, fallback) {
try {
return JSON.parse(raw) || fallback;
} catch {
return fallback;
}
}
console.log(parseJsonOrFallbackWrong("false", true)); // true, wrong
console.log(parseJsonOrFallbackWrong("0", 99)); // 99, wrong
console.log(parseJsonOrFallbackWrong("null", "fallback")); // "fallback", maybe wrong
false, 0, and null are valid JSON values. The fallback should not run just because the parsed value is falsy.
The safer helper returns directly from the try block:
function parseJsonOrFallback(raw, fallback) {
if (typeof raw !== "string") {
return fallback;
}
try {
return JSON.parse(raw);
} catch {
return fallback;
}
}
console.log(parseJsonOrFallback("false", true)); // false
console.log(parseJsonOrFallback("0", 99)); // 0
console.log(parseJsonOrFallback("null", "fallback")); // null
Sometimes a fallback hides too much. If the caller needs to know whether parsing failed, return a result object instead.
function safeJsonParse(raw) {
if (typeof raw !== "string") {
return {
ok: false,
error: new TypeError("Expected a JSON string"),
};
}
try {
return {
ok: true,
value: JSON.parse(raw),
};
} catch (error) {
return {
ok: false,
error,
};
}
}
const result = safeJsonParse(raw);
if (result.ok) {
console.log(result.value);
} else {
console.log("Could not parse JSON");
}
Use the fallback helper when recovery is obvious. Use the result object when the caller should decide what to do with the failure.
Parsing is not validation
Parsing answers one question:
Is this string valid JSON?
Validation answers a different question:
Is this value the shape my program expects?
This string parses successfully:
const parsed = JSON.parse('{"theme":42,"debug":"yes"}');
That does not mean it is valid settings data.
function isPlainObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function isSettings(value) {
return (
isPlainObject(value) &&
(value.theme === "light" || value.theme === "dark") &&
typeof value.reducedMotion === "boolean"
);
}
const defaultSettings = {
theme: "light",
reducedMotion: false,
};
const parsed = parseJsonOrFallback(rawSettings, null);
const settings = isSettings(parsed) ? parsed : defaultSettings;
JSON.parse did its job when it returned a JavaScript value. The validator does the application-specific job: checking that theme is allowed and reducedMotion is a boolean.
This is the line that keeps runtime data from leaking into the rest of your program with a fake sense of safety. TypeScript annotations, comments, and hopeful variable names do not validate an unknown string. Code has to inspect the value.
Where this fits with JSON, localStorage, and fetch
If you are learning the core conversion first, start with the JSON lesson. Stage 6 (working-with-data-json-06) introduces JSON.parse at /learn/working-with-data/json/6. Stage 11 (working-with-data-json-11-try-catch) covers catching parse errors at /learn/working-with-data/json/11. Stage 12 (working-with-data-json-12) asks you to write the safe parse helper at /learn/working-with-data/json/12.
If the string came from browser storage, pair this article with How to Store Objects in localStorage with JSON. That article covers the full storage round trip: stringify before saving, parse after reading, and provide a first-load fallback.
If the string came from a network response, keep HTTP status handling separate from JSON parsing. A 404 response is still a response, and the body might not be the success JSON shape your code expects. The fetch error handling guide covers that boundary.
A compact fetch example looks like this:
async function loadConfig() {
const response = await fetch("/api/config");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const text = await response.text();
const parsed = parseJsonOrFallback(text, null);
return isSettings(parsed) ? parsed : {
theme: "light",
reducedMotion: false,
};
}
The order is deliberate:
- First check whether the HTTP response belongs on the success path.
- Then parse the body text.
- Then validate the parsed value.
- Then choose the fallback if the value is not usable.
Those are four separate decisions. Combining them into one hopeful JSON.parse call is how small data bugs become weird UI behavior later.
Key Takeaways
JSON.parseparses valid JSON text. It does not repair JavaScript-ish strings with single quotes, comments, trailing commas, orundefined.- Malformed JSON throws a
SyntaxError, so parse unknown strings inside a smalltry/catchboundary. - Return fallback values only when parsing fails. Valid falsy JSON values like
false,0, andnullshould survive. - Parsing and validation are different jobs. Parse to get a JavaScript value, then validate that the value has the shape your code expects.
- Keep HTTP status checks, JSON parsing, validation, and fallback decisions separate enough that each failure tells the truth.