Three validation boundaries
Parsing JSON text checks a grammar. Validating a schema checks whether the parsed value has accepted fields, types and bounds. Transforming data applies the application operation. An exception or rejection at any of these stages does not automatically mean the text was malformed.
JSON.parse("null") succeeds and produces null. If the application requires a record, it must still reject it. Define outcome names narrowly: syntax for invalid JSON text, schema for a value violating the contract, and success for an accepted candidate. Programming errors during transformation must remain visible rather than becoming a misleading invalid-data message.
Shape guards and bounds
typeof value === "object" is also true for null and arrays. For an object returned by JSON.parse without a reviver, combine the null exclusion, object type and !Array.isArray(value) before inspecting fields as a record. Then validate every required field; the outer guard does not validate content.
Number.isInteger("3") is false: the method does not convert strings. Number.isInteger(3) is true, but a contract may still require a small range or Number.isSafeInteger. In the example, units from 0 to 100 is explicit. A nonempty string id is a separate rule: we do not convert it to a number or assume units validity establishes anything about id.
The thrown value and context
Throw can carry any language value. A catch receiving a string does not automatically receive an Error with message and stack. Using Error objects by convention supports a common contract, but a boundary receiving errors from external code must know or check their shape before reading fields.
new Error("save failed", { cause: original }) creates a new error and retains original in cause. This adds context without discarding the cause; it neither duplicates the original object nor guarantees identical stack formatting across engines. Choose a message explaining the failed operation and avoid treating the presence of a message as proof of recovery.
Catch bounds recovery
When throw is reached, following statements on that path are not executed and control seeks applicable handling. Catch does not resume at the statement immediately after throw. If the handler completes normally, execution can continue after try/catch.
Catching every exception and returning [] conflates a valid no-row result with unexpected failure. Handle only what is recoverable at that boundary and retain an explicit failure result when that is the contract. In the example, catch surrounds only JSON.parse: a known syntax failure is classified while any unexpected exception continues propagating.
Finally can replace the outcome
In this synchronous flow, finally is evaluated before leaving try, including when a return or throw is pending. If it completes normally, it preserves that earlier completion. If finally returns another value or throws another exception, its completion prevails. A return placed there can hide failure that should reach the caller.
Cleanup can also fail. A cleanup call throwing inside finally replaces the pending exception; the two errors are not automatically combined into an AggregateError. Define an explicit policy when both must be retained. Finally executes the final step specified by control flow; it does not provide automatic rollback or unconditional success.
Example: prepare without partial publication
decodeBatch parses JSON and validates the root as an array. Each row is validated before being projected into a new { id, units } object. The candidate array is local; a later rejection returns only ok false and does not publish already prepared rows. For a batch with units 2 followed by -1, the result is schema rather than success containing the first row.
The caller can retain a let current binding and assign current = result.value only when result.ok is true. This example is local, synchronous and free of external effects. A database write, remote publication or cross-process concurrency would require a different mechanism; JavaScript exception control does not make those operations transactional.
Diagnose by boundary and effect
To review a failure, identify the received value, the detecting instruction, mutations already performed and the consumer-visible outcome. An "invalid-json" message is suitable only when failure actually concerned syntax; an exception in a later transformation needs another classification.
In the questions, also compare state after failure. A push preceding throw remains in the array unless the program has an explicit policy to restore or discard that state. Distinguish unpublished local preparation from already visible mutations. This distinction supports choosing the answer satisfying all-or-nothing acceptance without promising guarantees the code does not provide.
function decodeBatch(text) {
let rows;
try {
rows = JSON.parse(text);
} catch (error) {
if (error instanceof SyntaxError) return { ok: false, reason: "syntax" };
throw error;
}
if (!Array.isArray(rows)) return { ok: false, reason: "schema" };
const candidate = [];
for (const row of rows) {
if (row === null || typeof row !== "object" || Array.isArray(row) ||
typeof row.id !== "string" || row.id.length === 0 ||
!Number.isInteger(row.units) || row.units < 0 || row.units > 100) {
return { ok: false, reason: "schema" };
}
candidate.push({ id: row.id, units: row.units });
}
return { ok: true, value: candidate };
}Assume text is a string and standard methods are used. The function accepts a JSON array, nonempty string IDs and integer Number units from 0 to 100; it projects only those two fields. Read a batch containing units 2 followed by -1 and determine whether any partial result is published.
Common pitfalls
Using typeof alone to accept objects; catching everything and returning empty success; expecting automatic rollback; returning in finally and hiding the pending outcome.
Related topics: Values, conversions and numeric contracts · Objects, identity and property boundaries · JSON, text and time contracts · Promises, async and error propagation
An error boundary must preserve classification and state. Independent preparation and bounded handling make the outcome interpretable.
Reference: The try statement · ECMAScript 2026 (17th edition); browser APIs identified separately