Errors are part of every program: invalid input, missing files, networks that drop. Good error handling isn't about catching everything — it's about catching errors where you can do something useful, and letting the rest travel upwards with enough context to debug them.
loadConfig fetches a config file. Note: fetch only rejects on network failure. A 404 or 500 still resolves — you must check res.ok yourself, as loadConfig does.
Throwing and catching
When something goes wrong, code throws an error. Execution stops at that line and the error travels up the call stack — out of the current function, into its caller, and so on — until something catches it.
try {
const config = JSON.parse(text);
start(config);
} catch (err) {
console.error('Could not start:', err.message);
} finally {
hideSpinner();
}
try— run this code.catch (err)— run this only if something intrythrew.finally— run this no matter what: success, error, or even areturninsidetry. The place for cleanup.
If nothing catches an error, it reaches the top: in a browser it's logged to the console; in Node it crashes the process.
Throw Error objects, not strings
throw new Error('User not found'); // ✓ has a message and a stack trace
throw 'User not found'; // ✗ no stack, no type
Built-in error types tell you what kind of problem it was: TypeError
(wrong kind of value), RangeError (number out of range), SyntaxError
(invalid JSON, for example). You can make your own:
class NotFoundError extends Error {
constructor(resource) {
super(`${resource} not found`);
this.name = 'NotFoundError';
}
}
try {
await loadUser(id);
} catch (err) {
if (err instanceof NotFoundError) showEmptyState();
else throw err; // not ours to handle — let it continue upwards
}
That last line matters: catch what you can handle, rethrow the rest. An
empty catch {} that swallows every error hides bugs for months.
Adding context with cause
A low-level error ("Unexpected token in JSON") rarely says what the program was trying to do. Wrap it, keeping the original:
function parseConfig(text) {
try {
return JSON.parse(text);
} catch (err) {
throw new Error('Invalid config file', { cause: err });
}
}
Now the error says what failed (Invalid config file) and err.cause
says why (the SyntaxError). Good logging prints the whole chain.
Async errors
Inside an async function, a thrown error becomes a rejected promise.
You handle it with try/catch around await, or .catch() on the
promise:
async function loadConfig() {
const res = await fetch('/config.json');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return parseConfig(await res.text());
}
try {
const config = await loadConfig();
} catch (err) {
showError(err);
}
Two async traps:
fetch doesn't reject on HTTP errors. A 404 or 500 is still a
successful fetch — the promise resolves. It only rejects if the request
couldn't be made at all (offline, DNS failure, CORS). Always check
res.ok.
Unhandled rejections. A promise that rejects with no handler triggers
an unhandledrejection event; in Node it crashes the process by default.
Every promise chain needs a .catch() or an await inside try
somewhere.
// ✗ fire-and-forget — the error goes nowhere
saveDraft(draft);
// ✓
saveDraft(draft).catch((err) => console.error('Draft not saved', err));
With Promise.all, the first rejection rejects the whole thing; use
Promise.allSettled when you want every result regardless of failures
(promises in depth).
A last line of defence
For errors nothing else caught, add global handlers that log and report:
window.addEventListener('error', (event) => report(event.error));
window.addEventListener('unhandledrejection', (event) => report(event.reason));
In React, an error boundary catches errors during rendering and shows a fallback instead of a blank page.
Rules of thumb
- Handle errors where you can recover or inform the user; elsewhere, let them propagate.
- Throw
Errorobjects, with messages that say what was being attempted. - Add context with
{ cause }instead of replacing the original error. - Check
res.okafterfetch. - Never leave a promise without a handler.
- Never swallow errors silently — at minimum, log them.