What you'll be able to do
By the end of this page you should be able to tell syntax, runtime, and logic errors apart, wrap risky code in try/catch, use finally when cleanup must run, and throw a clear error instead of returning a vague null with no explanation.
Previous post: pure vs impure functions.
Who this is for
- People who close the red console without reading it
- Beginners who wrap everything in
try/catchand leavecatchempty - Anyone who returns
nullon failure and then spends an hour finding where it started
You can skip this if you already throw meaningful errors, catch only where you can recover, and never swallow failures. Come back when a blank catch hides a real bug.
Three kinds of “wrong” (plain language)
Syntax error — the code is not valid JavaScript. The engine often refuses to run the script at all.
// function broken( { // missing pieces — will not parse
Scenario A — missing } or ). The console points near the problem; fix the spelling of the language first.
Runtime error — the code parsed, then crashed while running.
const user = null;
console.log(user.name); // TypeError
Scenario B — calling a method on null. Optional chaining helps; so does checking before use.
Logic error — the code runs, but the answer is wrong. No red text.
function average(a, b) {
return a + b / 2; // wrong formula
}
Scenario A — silent wrong math. Only tests and careful reading catch it. try/catch will not help here.
try / catch: recover when something throws
try {
JSON.parse("{ bad");
} catch (err) {
console.log("Could not parse:", err.message);
}
Scenario A — user-provided JSON. Parsing can throw. Catch, show a friendly message, keep the page alive.
Scenario B — code you control that should never throw. Do not hide it in catch. Fix the cause.
The err object usually has .message and a stack. Log or show the message; do not ignore err entirely.
finally: always run this cleanup
try {
// risky work
} catch (err) {
console.log(err.message);
} finally {
console.log("cleanup");
}
Scenario A — stop a loading spinner. Whether success or failure, turn it off in finally.
Scenario B — no catch, only finally. Possible, but beginners usually want catch when learning. Use finally when “must run either way” is the point.
throw: make failure loud and clear
function double(n) {
if (typeof n !== "number" || Number.isNaN(n)) {
throw new Error("double() needs a real number");
}
return n * 2;
}
try {
console.log(double("x"));
} catch (err) {
console.log(err.message);
}
Scenario A — invalid input. Throwing beats returning undefined with no clue.
Scenario B — call sites that can recover. They catch and show UI. Call sites that cannot recover let it bubble to the console — still better than silent wrong data.
Prefer throw new Error("...") with a human message. Empty throw "nope" works but Error stacks are easier to debug.
Never swallow errors
try {
doWork();
} catch (err) {
// empty — worst habit
}
Scenario A — “I’ll handle it later.” Later never comes; the bug vanishes.
Scenario B — minimum honesty. Log err, show a message, or rethrow: throw err.
Catch only where you have a real next step. Otherwise let the error surface.
A tiny practice file
Save as errors.html. Type JSON; click to parse safely.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Errors</title>
</head>
<body>
<input id="raw" placeholder='{"ok":true}' style="width: 16rem" />
<button id="go">Parse JSON</button>
<p id="out">Result will show here.</p>
<script>
function parseUserJson(text) {
if (text.trim().length === 0) {
throw new Error("Type some JSON first.");
}
return JSON.parse(text);
}
document.getElementById("go").addEventListener("click", function () {
const out = document.getElementById("out");
const raw = document.getElementById("raw").value;
try {
const data = parseUserJson(raw);
out.textContent = "Parsed OK: " + JSON.stringify(data);
} catch (err) {
out.textContent = "Failed: " + err.message;
}
});
</script>
</body>
</html>
Try valid JSON, empty input, and {bad — you should see clear failure text, not a frozen page.
Mistakes I see a lot
1. Empty catch. Hides bugs. Log or rethrow.
2. try/catch around code that cannot throw. Noise without benefit.
3. Catching and returning null with no log. The next function fails farther from the cause.
4. Ignoring logic errors. Green console ≠ correct program.
5. Throwing strings only. Prefer new Error("...").
6. Catching too high and showing a generic “Error” for everything. Keep messages specific when you throw.
What to try before the next post
- Force a
TypeErrorwithnull.xand read the message. throw new Error("demo")insidetry/catch.- Add a
finallythat always logs"done". - Build the tiny HTML file and break the JSON on purpose.
This closes Phase 4 of the series (functions). Next: arrays — basics and mutation.
Try this next outside the series
Functions matter more once they stop being textbook examples and start shaping reusable app code.
- Node.js async patterns — see functions return promises, accept callbacks, and coordinate real work
- Diff Checker — compare two function versions before you keep a refactor