Error Handling & Debugging Patterns
Every application has errors. Senior engineers design systems that handle them gracefully â surfacing actionable information, preventing cascading failures, and maintaining user experience.
The Error Object Model
const err = new Error('Something went wrong');
err.message; // "Something went wrong"
err.name; // "Error"
err.stack; // Stack trace string
err.cause; // undefined (ES2022 â set via options)Built-in Error Types
| Type | When Thrown |
|---|---|
TypeError | Wrong type operation (null.prop, calling non-function) |
ReferenceError | Accessing undeclared variable |
SyntaxError | Invalid syntax (usually at parse time) |
RangeError | Value outside valid range (new Array(-1)) |
URIError | Malformed URI functions |
AggregateError | Multiple errors (Promise.any rejection) |
Custom Error Classes
class AppError extends Error {
constructor(message, { code, statusCode, cause } = {}) {
super(message, { cause });
this.name = 'AppError';
this.code = code;
this.statusCode = statusCode;
}
}
class NotFoundError extends AppError {
constructor(resource, id, options) {
super(`${resource} with id ${id} not found`, {
code: 'NOT_FOUND',
statusCode: 404,
...options,
});
this.name = 'NotFoundError';
}
}
class ValidationError extends AppError {
constructor(errors, options) {
super('Validation failed', {
code: 'VALIDATION_ERROR',
statusCode: 400,
...options,
});
this.name = 'ValidationError';
this.errors = errors;
}
}Error cause (ES2022)
Chain errors to preserve the original failure context:
async function fetchUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (error) {
throw new AppError('Failed to fetch user', {
code: 'FETCH_ERROR',
cause: error,
});
}
}Async Error Handling
Promise Rejection Patterns
async function loadData() {
const [users, posts] = await Promise.all([
fetchUsers().catch(err => {
console.error('Users failed:', err);
return [];
}),
fetchPosts().catch(err => {
console.error('Posts failed:', err);
return [];
}),
]);
return { users, posts };
}Promise.allSettled for Resilient Batch Operations
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(2),
fetchUser(999),
]);
const succeeded = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);Unhandled Rejection Detection
window.addEventListener('unhandledrejection', event => {
reportError({
type: 'unhandled_promise_rejection',
reason: event.reason,
promise: event.promise,
});
event.preventDefault();
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled rejection', { reason, promise });
});Error Boundary Patterns
React Error Boundaries
class ErrorBoundary extends React.Component<
{ fallback: ReactNode; children: ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
reportError({ error, componentStack: info.componentStack });
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}Granular Error Boundaries
<ErrorBoundary fallback={<AppCrashScreen />}>
<Header />
<ErrorBoundary fallback={<SidebarFallback />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError onRetry={refetch} />}>
<MainContent />
</ErrorBoundary>
</ErrorBoundary>The Result Pattern (No-Throw)
Inspired by Rust's Result<T, E>, avoid throwing in expected failure paths:
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function safeJson<T>(response: Response): Promise<Result<T>> {
try {
if (!response.ok) {
return { ok: false, error: new Error(`HTTP ${response.status}`) };
}
const data = await response.json();
return { ok: true, value: data as T };
} catch (error) {
return { ok: false, error: error as Error };
}
}
const result = await safeJson<User>(response);
if (result.ok) {
renderUser(result.value);
} else {
showError(result.error.message);
}Global Error Monitoring
window.addEventListener('error', event => {
reportError({
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error,
});
});
window.addEventListener('unhandledrejection', event => {
reportError({
type: 'promise_rejection',
reason: event.reason,
});
});Debugging Strategies
Beyond console.log
console.table(arrayOfObjects);
console.group('API Call');
console.time('fetch');
await fetch(url);
console.timeEnd('fetch');
console.groupEnd();
console.assert(user.age > 0, 'Age must be positive', user);
console.trace('Execution path');Conditional Breakpoints
In DevTools, right-click a line â "Add conditional breakpoint" â enter a condition:
user.id === 42 && user.status === 'active'Performance Profiling
performance.mark('render-start');
renderComponent();
performance.mark('render-end');
performance.measure('render-time', 'render-start', 'render-end');
const entries = performance.getEntriesByName('render-time');
console.log(`Render took ${entries[0].duration}ms`);Interview Signal
Senior candidates demonstrate:
- Error hierarchy â Custom error classes with
causechaining, not justthrow new Error() - Async awareness â
Promise.allSettledfor resilience, global rejection handlers - Boundary architecture â Granular error boundaries in React, not one giant wrapper
- Result pattern â No-throw alternatives for expected failures vs exceptions for unexpected ones
- Monitoring story â Global handlers, structured error reporting, source maps in production