Exceptions interrupt ordinary execution until a matching catch handles them. Use throw for a failure that prevents an operation from fulfilling its contract; use try...catch where you can recover or translate that failure meaningfully. Catching every exception and returning a success-shaped default makes a program appear healthy while losing information.
External input has two separate questions: is it valid syntax, and does it have the shape and meaning the program requires? Parsing JSON answers the first question only. A syntactically valid JSON number is not a valid study-session record.
function parseSession(text) {
const value = JSON.parse(text);
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new Error("Expected one session object.");
}
if (typeof value.topic !== "string" || value.topic.trim() === "") {
throw new Error("Enter a topic.");
}
if (!Number.isInteger(value.minutes) || value.minutes < 1 || value.minutes > 180) {
throw new Error("Duration must be a whole number from 1 to 180.");
}
return { topic: value.topic.trim(), minutes: value.minutes };
}
for (const text of ['{"topic":" JS ","minutes":25}', '{"topic":"JS","minutes":0}']) {
try {
console.log(parseSession(text));
} catch (error) {
console.log(error instanceof Error ? error.message : "Unknown failure");
}
}
The successful value contains topic JS and duration 25; the second input prints the duration error. The returned object is newly constructed rather than an unexamined copy of every input property. This boundary deliberately keeps only fields the application understands. Whether additional fields should instead be rejected is an API-contract decision.
Own recovery and cleanup
A catch variable can contain any thrown JavaScript value, not only an Error. Check before reading .message. Throw Error objects in code you control because they carry a useful message and stack. Preserve the original error as a cause when adding context, rather than destroying its diagnostic information. Do not send internal stacks, credentials or complete personal payloads to an end user or a public log.
finally runs when control leaves a try/catch through ordinary completion, return or throw, making it useful for cleanup. A return or throw inside finally can replace the earlier result; avoid that unless you explicitly need this behavior. A promise rejection is not caught by a surrounding synchronous try unless you await the promise or attach a rejection handler. The next chapters show that relationship.
Do not use exceptions to disguise a predictable absence. A lookup can return undefined, and the caller can handle "not found" as an ordinary outcome. Conversely, allowing invalid state to continue and hoping a later operation fails produces errors far from their cause. Validate at entry points and preserve invariants inside the program.
Check the limits of a validator
This example bounds the numeric duration and trims text, but does not impose a topic-length limit or reject all control characters. Requirements such as maximum request size, authorization and durable storage belong to the complete application boundary. A type checker later helps callers of a validated function; it does not replace these runtime checks.
Exercise: add a maximum topic length of 80 characters, reject arrays and malformed JSON, and verify that a failed record never enters a collection of accepted sessions.
Solution guidance: perform all validation before appending. Test null, an array, a numeric topic, a fractional duration and a blank topic. Keep syntax errors distinguishable during debugging, while giving a useful safe message in a UI.
Reference: Control flow and error handling.
Previous: Modules, Packages and Project Tooling. Next: Promises, async/await and the Event Loop.

