Handle JSON.parse Errors Without Hiding Bad Data

How should you handle JSON.parse errors?
Keep malformed text separate from a valid document that fails your application's rules. In the helper below, return a specific failure stage, validate the required fields, and pass only the accepted fields onward. Keep later work outside the parsing catch so a bug in that work is not reported as bad input.
This tutorial builds an original, deliberately small print-job importer. It processes local strings only: nothing is fetched, printed or saved. The examples and failure cases were checked in Node.js v22.18.0. That identifies the test environment, not a minimum supported version or a browser compatibility test.
What does parsing establish?
MDN's JSON.parse reference says invalid JSON throws a SyntaxError. Successful parsing can return an object, array, string, number, boolean or null. Success alone therefore does not establish that you received the document your application expects.
Our teaching contract requires an object containing label, a string with non-whitespace content, and copies, an integer from 1 through 20. The label is trimmed in the returned record. Additional fields are ignored, not forwarded. These choices belong to this example; JSON does not prescribe print-job requirements.
How can the importer report separate failure stages?
Run this complete helper with ordinary, unmodified JavaScript built-ins:
function readPrintJob(text) {
if (typeof text !== "string") {
return { ok: false, stage: "input" };
}
let value;
try {
value = JSON.parse(text);
} catch (error) {
if (!(error instanceof SyntaxError)) throw error;
return { ok: false, stage: "syntax" };
}
if (value === null || typeof value !== "object" ||
Array.isArray(value)) {
return { ok: false, stage: "shape" };
}
if (typeof value.label !== "string" ||
value.label.trim().length === 0 ||
!Number.isSafeInteger(value.copies) ||
value.copies < 1 || value.copies > 20) {
return { ok: false, stage: "shape" };
}
return {
ok: true,
value: { label: value.label.trim(), copies: value.copies }
};
}
The first guard makes string input an explicit requirement of this helper. A caller that already has an object should use a separate object-validation interface rather than sending it through this text interface.
Array.isArray identifies arrays. Here the object gate explicitly excludes arrays and null before examining fields. The resulting check is for this parsed-document contract, not a general validator for arbitrary objects, proxies or class instances.
Number.isSafeInteger checks for a numeric safe integer. The separate comparisons enforce our narrower 1–20 policy. Consequently, a numeric string such as "2" is rejected instead of converted. If conversion is wanted, specify and test that different policy explicitly.
Why keep the catch block narrow?
MDN's try...catch reference explains that an exception from any statement inside try, including a called function, transfers control to catch. It also describes rethrowing errors outside the expected subset.
Our try contains only parsing. Validation follows it, and actual application work belongs after the helper returns. This prevents a later printing or rendering bug from being classified by this helper as malformed text. The tagged result is our interface design; JavaScript does not supply those stage names.
Do not add a catch-all fallback that returns a successful empty job. The caller needs to know whether to request text, request a corrected document, or investigate a separate application failure. Returning { ok: false } also creates a responsibility: the caller must inspect ok before using value.
Which cases should you check?
Append this demonstration after the helper:
console.log(readPrintJob('{"label":" Map ","copies":2}'));
console.log(readPrintJob('{"label":"Map",}'));
console.log(readPrintJob('null'));
console.log(readPrintJob('{"label":"Map","copies":"2"}'));
The first result is { ok: true, value: { label: "Map", copies: 2 } }. The remaining stages are syntax, shape and shape, respectively. Assert returned values in tests rather than depending on console formatting.
| Additional fixture | Expected result from this helper |
|---|---|
| An object passed directly | input |
| An empty string | syntax |
The text [] |
shape |
| A blank label or missing copies | shape |
| Copies 0, 21 or 1.5 | shape |
| Copies 1 or 20 with a valid label | Accepted |
An extra debug field |
Accepted, with that field absent from the result |
Those branches were checked locally. The fixture values are invented application tests, not a production payload sample. An accepted record proves only these checks passed; it does not grant permission to print, establish available stock, or make subsequent operations succeed.
Where does this fit in a larger application?
Handle a transport failure before this text-processing stage. Our Fetch cancellation guide covers request ownership and response handling. Do not label a failed request as a malformed document when no document was obtained.
Use optional chaining for genuinely optional paths, rather than concealing missing required fields. For multiple imports, choose the sequencing and reporting policy described in async array processing; this synchronous helper does not schedule a batch.
For a user-facing failure, report the stage and a useful correction request. Our recommendation is to keep raw submitted text out of routine logs, since a real input may contain private information. The demonstration needs no real customer records, credentials or external service. Extend the contract and its tests before adapting it to a consequential workflow.