Error Handling
Things go wrong: networks fail, users type unexpected input, data arrives in the wrong shape. Good error handling means anticipating failures, reporting them clearly, and keeping the rest of the program working. JavaScript's tools are throw, try/catch/finally, and the Error family of objects.
What happens without handling
const settings = JSON.parse('{"theme": "dark"}'); console.log("theme:", settings.theme); const broken = JSON.parse("{theme: dark}"); console.log("this line never runs");
theme: dark Uncaught SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
An uncaught error stops the current script at that line. In a browser, other event handlers keep working, but whatever that code was meant to do is left half-done.
try, catch and finally
function parseSettings(text) { try { const parsed = JSON.parse(text); return parsed; } catch (error) { console.log(`Bad settings (${error.name}), using defaults`); return { theme: "light" }; } finally { console.log("parse attempt finished"); } } console.log(parseSettings('{"theme":"dark"}')); console.log(parseSettings("not json"));
parse attempt finished
{ theme: 'dark' }
Bad settings (SyntaxError), using defaults
parse attempt finished
{ theme: 'light' }trywraps code that might fail.catchruns only if something intrythrew. It receives the error. If you don't need it, write justcatch { … }.finallyalways runs — after success, after an error, even after areturn. Use it for cleanup like closing connections or re-enabling buttons.
Error objects
Every error has a name, a message and usually a stack trace showing where it happened. JavaScript's built-in types tell you what went wrong:
| Type | Typical cause |
|---|---|
TypeError | Using a value the wrong way: calling a non-function, reading a property of undefined |
ReferenceError | Using a variable that doesn't exist |
SyntaxError | Invalid code, or invalid JSON passed to JSON.parse |
RangeError | A number outside its allowed range: new Array(-1), toFixed(200) |
const tests = [ () => null.length, () => missingVariable, () => (5).toFixed(500), ]; for (const test of tests) { try { test(); } catch (err) { console.log(`${err.name}: ${err instanceof TypeError ? "type problem" : "other"}`); } }
TypeError: type problem ReferenceError: other RangeError: other
Throwing your own errors
Use throw when your code detects a problem it can't sensibly handle itself. Always throw Error objects (not strings), so you get a stack trace:
function withdraw(balance, amount) { if (typeof amount !== "number" || Number.isNaN(amount)) { throw new TypeError("Amount must be a number"); } if (amount > balance) { throw new RangeError(`Cannot withdraw ${amount}; balance is ${balance}`); } return balance - amount; } for (const amount of [30, 500, "ten"]) { try { console.log("new balance:", withdraw(100, amount)); } catch (err) { console.log(`${err.name}: ${err.message}`); } }
new balance: 70 RangeError: Cannot withdraw 500; balance is 100 TypeError: Amount must be a number
Custom error classes
Subclassing Error lets callers tell different failures apart and carry extra information:
class HttpError extends Error { constructor(status, message) { super(message); this.name = "HttpError"; this.status = status; } } function handle(err) { if (err instanceof HttpError && err.status === 404) return "Show 'not found' page"; if (err instanceof HttpError && err.status >= 500) return "Show 'try again later'"; throw err; } console.log(handle(new HttpError(404, "No such page"))); console.log(handle(new HttpError(503, "Down for maintenance")));
Show 'not found' page Show 'try again later'
Notice throw err at the end: handle only the errors you understand and rethrow the rest. Silently swallowing unknown errors hides bugs.
Wrapping errors with cause
When you catch a low-level error and throw a more meaningful one, keep the original as its cause:
function loadConfig(text) { try { return JSON.parse(text); } catch (err) { throw new Error("Config file is corrupted", { cause: err }); } } try { loadConfig("{oops"); } catch (err) { console.log(err.message, "← caused by", err.cause.name); }
Config file is corrupted ← caused by SyntaxError
Async errors
try/catch only catches errors thrown synchronously inside it, or by an awaited promise. A callback that runs later is outside it:
try { setTimeout(() => { try { throw new Error("inside timer"); } catch (err) { console.log("caught inside the callback:", err.message); } }, 0); } catch { console.log("never reached — the timer ran later"); } async function main() { try { await Promise.reject(new Error("rejected promise")); } catch (err) { console.log("await + try/catch works:", err.message); } } main();
await + try/catch works: rejected promise caught inside the callback: inside timer
Global safety nets
As a last resort — for logging, not normal handling — browsers fire events for errors nobody caught:
window.addEventListener("error", event => { reportToServer({ message: event.message, file: event.filename, line: event.lineno }); }); window.addEventListener("unhandledrejection", event => { reportToServer({ message: String(event.reason) }); });
Error-monitoring services such as Sentry hook into these events to collect crashes from real users.
Guidelines
- Catch errors where you can do something useful: retry, show a message, use a fallback.
- Show users a friendly message; log the technical details for developers.
- Never leave a
catchblock empty without a comment explaining why. - Validate input early and throw clear errors, rather than letting bad data cause confusing failures later.