Programatium Workshop
Language Tools

Handle JSON.parse Errors Without Hiding Bad Data

Handle JSON.parse Errors Without Hiding Bad Data
In briefKeep 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.

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.

Sources

FAQ

Why does parsing null not report a syntax error?

The text null is valid JSON, and JSON.parse returns the JavaScript value null. This importer rejects that result at its shape stage because its own contract requires an object with particular fields. Keep those two decisions separate when explaining a failed import to a caller.

Why reject copies supplied as a numeric string?

Number.isSafeInteger does not accept a string as a numeric safe integer. This example deliberately requires a number and does not convert the input. For a different application, define whether conversion is allowed, what representations it accepts, and which tests demonstrate the intended behavior before changing the helper.

Should every failed import become an empty object?

That would erase the failure distinction this example was designed to preserve. Return the failure result to the caller, inspect its stage, and choose an appropriate response. Our importer never treats a failed check as a successful empty job, and its accepted result still does not authorize an external action.