JavaScript Error Handling: try/catch, Custom Errors, and Patterns
A deep, practical guide to error handling in JavaScript for frontend interviews: sync vs async errors, try/catch with async/await, custom error classes, cause, and user-friendly error patterns.
Frontend Interview Team
February 08, 2026
Error handling is a “senior signal” topic.
30‑second interview answer
try/catch catches synchronous errors. For async, either await inside try/catch or attach .catch() to the promise. In real apps, create meaningful error types (custom Error classes), keep user-facing messages safe, and log enough context without leaking secrets.
Key points
- Sync throw → try/catch.
- Promise rejection →
await+ try/catch OR.catch. - Prefer typed errors with
name/message/cause. - Separate user message from debug details.
Interviewers want to see if you can:
- catch errors correctly (sync vs async)
- create meaningful error types
- build user-friendly fallback UI
- log without leaking sensitive data
1) The basics: throw, try/catch
function parseJson(str) {
try {
return JSON.parse(str);
} catch (err) {
return null;
}
}Key idea:
try/catchcatches synchronous errors thrown inside the try block.
2) Async errors: Promises vs try/catch
A) Promise style
fetch('/api')
.then((r) => r.json())
.catch((err) => {
console.error('Request failed', err);
});B) async/await style
async function load() {
try {
const res = await fetch('/api');
if (!res.ok) throw new Error('Bad response');
return await res.json();
} catch (err) {
console.error(err);
throw err;
}
}Interview tip:
try/catchworks withawaitbecause the awaited promise rejection behaves like a thrown error.
3) Common mistake: try/catch doesn’t catch async without await
try {
fetch('/api').then(() => {
throw new Error('boom');
});
} catch (err) {
// won't run
}Because the throw happens later, outside the current sync call stack.
Fix:
- use
.catch() - or
awaitinsidetry/catch
4) Create custom errors (this is interview gold)
Custom error classes make debugging and UI decisions easier.
class HttpError extends Error {
constructor(message, { status, url, cause } = {}) {
super(message, { cause });
this.name = 'HttpError';
this.status = status;
this.url = url;
}
}
async function fetchJson(url) {
const res = await fetch(url);
if (!res.ok) {
throw new HttpError('Request failed', { status: res.status, url });
}
return res.json();
}Now the caller can do:
try {
await fetchJson('/api/user');
} catch (err) {
if (err instanceof HttpError && err.status === 401) {
// show login UI
}
}5) cause (modern JS)
Modern Error supports cause:
try {
doThing();
} catch (err) {
throw new Error('Failed to doThing', { cause: err });
}This helps preserve the original failure while adding context.
6) Pattern: convert unknown errors into a safe shape
In real apps, catch (err) gives unknown in TypeScript.
You should normalize it.
function toErrorMessage(err: unknown) {
if (err instanceof Error) return err.message;
return String(err);
}7) Frontend patterns: user-friendly errors
A) Show retryable states
- network errors → “Retry”
- empty states → “No results”
B) Log errors, but don’t expose secrets
- avoid logging tokens, PII
- redact request headers
C) Errors as part of state
const [state, setState] = useState({ loading: true, data: null, error: null });
try {
const data = await load();
setState({ loading: false, data, error: null });
} catch (err) {
setState({ loading: false, data: null, error: toErrorMessage(err) });
}8) Interview Q&A
Q: What’s the difference between throwing and rejecting?
- Throwing in async function becomes a rejected promise.
Q: How do you handle errors from fetch?
fetchonly rejects on network errors. For HTTP errors, checkres.okand throw.
Q: When do you use custom errors?
- When different failure types need different UI/retry behavior.
Summary checklist
- I can explain sync vs async error handling.
- I can explain
fetchHTTP errors vs network errors. - I know when to create custom errors.
- I separate user-friendly messages from debug details.
Summary
try/catchcatches sync errors and awaited async errors.- Use
.catchfor promise chains. - Create custom errors for better control and debugging.
- Normalize unknown errors for safe UI.
- Always design user-friendly fallback states.