Try / Catch / Finally — The Core
What they do
try— run code that might fail.catch (err)— runs only if something intrythrows.finally— runs always (aftertry, or aftercatch), even if youreturnorthrow—great for cleanup.
function readConfig(raw) {
try {
const cfg = JSON.parse(raw); // might throw SyntaxError
if (!cfg.url) throw new Error("url missing");
return cfg;
} catch (err) {
// handle or rethrow with context
throw new Error(`Invalid config: ${err.message}`);
} finally {
// cleanup: metrics, timers, locks, temp files
console.log("parse attempt finished");
}
}
Key rules (that trip people up)
finallyalways runs, even if youreturninsidetry/catch.- A
returninsidefinallyoverrides earlier returns/throws — avoid doing that. try…catchonly catches synchronous errors. For async code, useawaitor.catch.- In async functions,
await something()insidetrywill route rejections tocatch.
// ❌ catch does NOT catch the async error:
try {
fetch('/api'); // returns a Promise, no await; rejection is unhandled here
} catch (e) {} // never reached
// ✅ correct:
try {
const res = await fetch('/api');
} catch (e) {
// handles network errors / rejections
}
Built‑in Error Types (JS)
Error— the base class (generic failures).TypeError— wrong type (e.g., calling non‑function).ReferenceError— variable not defined.SyntaxError— invalid code orJSON.parse()failure.RangeError— value out of range (e.g., invalid array length).URIError— baddecodeURI/encodeURIinput.EvalError— legacy (rare).AggregateError— multiple errors together (e.g., fromPromise.any).
try {
JSON.parse("{ bad json }");
} catch (e) {
if (e instanceof SyntaxError) {
console.error("Bad JSON:", e.message);
}
}
In browsers you’ll also see
DOMExceptionfor things like storage/quota/permission issues. In Node.js you may seeSystemError-like errors withcode(e.g.,ENOENTfor missing file).
Adding Context with cause (modern JS)
Wrap lower-level errors with more context while keeping the original stack:
try {
login(user, pass);
} catch (e) {
throw new Error("Failed to login user", { cause: e });
}
// Later:
function toClientError(err) {
console.error(err.cause); // original error
return { message: err.message };
}
Extending Errors (Custom Error Classes)
Create domain‑specific errors to make handling cleaner.
class ValidationError extends Error {
field: string;
constructor(message: string, field: string, cause?: unknown) {
super(message, { cause });
this.name = "ValidationError";
this.field = field;
}
}
class ExternalApiError extends Error {
status: number;
constructor(message: string, status: number, cause?: unknown) {
super(message, { cause });
this.name = "ExternalApiError";
this.status = status;
}
}
// Usage
function assertEmail(email: string) {
if (!/^\S+@\S+\.\S+$/.test(email)) {
throw new ValidationError("Invalid email", "email");
}
}
Why this helps: In one place you can switch on instanceof and respond appropriately (400 vs 500, retry vs fail fast, etc.).
Real‑World Scenarios & Tiny Examples
1) Parsing user input (validation path)
function parseAndValidate(formJson) {
try {
const data = JSON.parse(formJson); // may throw
if (!data.email) throw new ValidationError("Email required", "email");
return data;
} catch (err) {
// Map to user-friendly message
if (err instanceof SyntaxError) return { error: "Bad JSON format" };
if (err instanceof ValidationError) return { error: err.message, field: err.field };
return { error: "Unexpected error" };
}
}
2) HTTP request with retries (transient failures)
async function getWithRetry(url, tries = 3) {
let lastErr;
for (let i = 0; i < tries; i++) {
try {
const res = await fetch(url);
if (!res.ok) throw new ExternalApiError("Bad status", res.status);
return await res.json();
} catch (e) {
lastErr = e;
// retry only for network / 5xx
if (e instanceof ExternalApiError && e.status < 500) break;
await new Promise(r => setTimeout(r, 300 * (i + 1)));
}
}
throw new Error(`All retries failed`, { cause: lastErr });
}
3) File handling with guaranteed cleanup
import fs from "node:fs/promises";
async function processTempFile(content) {
const path = `/tmp/upload-${Date.now()}.txt`;
try {
await fs.writeFile(path, content);
// … process file …
} finally {
// ensure temp file removed even if processing throws
await fs.rm(path).catch(() => {});
}
}
4) Express.js centralized error handler (Node)
// route
app.get("/users/:id", async (req, res, next) => {
try {
const user = await repo.findUser(req.params.id);
if (!user) throw new ValidationError("User not found", "id");
res.json(user);
} catch (e) {
next(e); // pass to error middleware
}
});
// error middleware (last)
app.use((err, req, res, next) => {
console.error(err); // include err.cause if present
if (err instanceof ValidationError) {
res.status(400).json({ error: err.message, field: err.field });
} else if (err instanceof ExternalApiError) {
res.status(502).json({ error: "Upstream failure" });
} else {
res.status(500).json({ error: "Internal error" });
}
});
5) React component boundary (UI safety net)
UI note: React Error Boundaries catch render/lifecycle errors of children, not event handlers / async. Use try/catch inside event handlers.
function SaveButton() {
const onClick = async () => {
try {
await save();
alert("Saved!");
} catch (e) {
alert("Save failed: " + (e.message ?? "Unknown"));
}
};
return <button onClick={onClick}>Save</button>;
}
Patterns & Best Practices
- Fail fast on programmer errors, handle gracefully on user/network errors.
- Don’t swallow errors: if you can’t recover, rethrow or bubble up.
- Attach context (
cause, extra fields) so logs are actionable. - Use custom errors for predictable branching (
instanceof). - Use
finallyfor deterministic cleanup (files, locks, spinners, timers). - In async code, always
awaitinside thetry. - Prefer narrow try blocks (wrap only the lines that can throw).
- For concurrency, consider
Promise.allSettledto collect failures. - Map internal errors to safe client messages, keep details in logs.
Subtle Behaviors (Know these)
// finally overriding returns — avoid this!
function f() {
try { return 1; }
finally { return 2; } // overrides → returns 2
}
// thrown in finally replaces earlier error — dangerous
function g() {
try { throw new Error("A"); }
finally { throw new Error("B"); } // "B" only — "A" is lost
}
25 Interview Qs (with short, direct answers)
-
What does
try…catchdo? Runs code that may throw;catchhandles the thrown error. -
When does
finallyrun? Always (aftertryorcatch), even if there’s areturnorthrow. -
Does
try…catchcatch async errors? No—only if youawaitthe promise (or use.catch). -
Name 5 built‑in error types.
Error,TypeError,ReferenceError,SyntaxError,RangeError. -
When do you use
TypeErrorvsError? UseTypeErrorfor wrong types (e.g., calling non‑function),Errorwhen generic. -
How to preserve original error details when rethrowing? Wrap with
new Error("context", { cause: err }). -
What happens if
finallyhas areturn? It overrides earlierreturn/throw. Avoid it. -
How to catch errors from multiple async ops without failing fast? Use
Promise.allSettledand inspect results. -
Difference:
throw new Error()vsreturn Promise.reject()in async fn In anasyncfunction both become rejected promises, butthrowis clearer and keeps stack semantics. -
How to classify errors cleanly in an API? Create custom errors (
ValidationError,ExternalApiError) and map them to HTTP codes. -
What is
AggregateErrorused for? To hold several errors together (e.g., fromPromise.any). -
How to retry only on transient errors? Catch, check error type/status, backoff, retry; otherwise rethrow.
-
Why keep
tryblocks small? So you don’t accidentally hide unrelated errors and to keep intent clear. -
How to guarantee resource cleanup? Use
finally(close file/conn, release lock, stop timer). -
Do
catchparameterless andcatch (e)differ?catch {}(no param) is allowed if you don’t need the error object. -
What is a “swallowed” error? You catch but neither fix nor rethrow/log → it disappears; avoid this.
-
How to attach metadata to errors? Subclass
Errorwith fields (e.g.,status,field), or set properties. -
Can
JSON.parseerrors be distinguished from others? Yes:err instanceof SyntaxError. -
How to centralize error handling in Express?
next(err)and define a final error middleware. -
React: what don’t Error Boundaries catch? Async/event handler errors. Use local try/catch in handlers.
-
Why use
causeover concatenating messages? Keeps original stack and structured provenance. -
What happens to the stack when you
throw? The stack is captured at throw; wrapping withcausepreserves original. -
How to make a user‑friendly message while logging technical detail? Log full error (and
cause), return sanitized message to user. -
When to prefer
allSettledvsall?allSettledwhen you want all outcomes without short‑circuit on first failure. -
How to test error paths? Mock dependencies to throw/reject; assert that you handle/transform correctly.
Mini “Templates” You Can Reuse
1) Validate → Map → Throw
function requireEnv(key: string) {
const v = process.env[key];
if (!v) throw new ValidationError(`Missing env ${key}`, key);
return v;
}
2) Wrap with context
try {
await doPayment();
} catch (e) {
throw new Error("Payment failed for order 123", { cause: e });
}
3) Node CLI safe main
async function main() {
try { /* ... */ }
catch (e) { console.error(e); process.exitCode = 1; }
}
main().finally(() => console.log("done"));
4) Promise utilities
const safe = (p) => p.then(v => [v, null]).catch(e => [null, e]);
const [data, err] = await safe(fetchJSON(url));
if (err) /* handle */;
When to Use What (Cheat Sheet)
- User input / JSON / config →
try…catch,SyntaxError/ValidationError. - External API / DB →
try…catchwithawait, customExternalApiError, retries + backoff. - File/stream/temp →
finallyfor cleanup. - Batch ops →
Promise.allSettledto collect per‑item errors. - Web API (Express) → throw custom errors in routes, central error middleware.
- UI (React) → try/catch in event handlers; Error Boundaries for render crashes.