JavaScript error handling is often an afterthought — a try/catch here and there. But a robust error handling strategy makes the difference between applications that crash silently and applications that degrade gracefully. Firefox DevTools shows you exactly what went wrong and where.
Error Types
// The built-in error types
new Error('Generic error') // Base class
new TypeError('Expected a string') // Wrong type
new RangeError('Index out of bounds') // Value out of range
new ReferenceError('x is not defined') // Undefined variable
new SyntaxError('Unexpected token') // Invalid syntax (usually parse errors)
new URIError('Malformed URI') // Bad URI
new EvalError('eval() error') // eval()-related
// Custom error types
class DatabaseError extends Error {
constructor(message, { query, code } = {}) {
super(message);
this.name = 'DatabaseError';
this.query = query;
this.code = code;
}
}
class NetworkError extends Error {
constructor(message, { status, url } = {}) {
super(message);
this.name = 'NetworkError';
this.status = status;
this.url = url;
}
}
Try/Catch/Finally
try {
const result = riskyOperation();
process(result);
} catch (error) {
if (error instanceof TypeError) {
// Handle type errors specifically
console.error('Type error:', error.message);
} else if (error instanceof NetworkError) {
// Handle network errors
showNetworkErrorUI(error);
} else {
// Re-throw unknown errors
throw error;
}
} finally {
cleanup(); // Always runs, even if there's an error
}
Async Error Handling
Errors in async functions behave differently:
// Unhandled promise rejection — appears as a red error in DevTools
async function badExample() {
throw new Error('Oops'); // Creates unhandled rejection if not awaited with try/catch
}
badExample(); // No try/catch = unhandled rejection
// Correct: always handle async errors
async function main() {
try {
await badExample();
} catch (error) {
console.error('Caught:', error);
}
}
// Or with .catch()
badExample().catch(error => console.error('Caught:', error));
The Result Pattern (Tuple Pattern)
Instead of throwing, return [value, null] on success and [null, error] on failure:
async function safeJsonParse(text) {
try {
return [JSON.parse(text), null];
} catch (error) {
return [null, error];
}
}
async function safeFetch(url) {
try {
const response = await fetch(url);
if (!response.ok) {
return [null, new NetworkError(`HTTP ${response.status}`, { status: response.status, url })];
}
const data = await response.json();
return [data, null];
} catch (error) {
return [null, error];
}
}
// Usage: no nested try/catch at call sites
const [data, error] = await safeFetch('/api/users');
if (error) {
handleError(error);
return;
}
processUsers(data);
Global Error Handling
Catch errors that escape local try/catch:
// Synchronous errors
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
reportToMonitoring({
message: event.message,
filename: event.filename,
line: event.lineno,
col: event.colno,
stack: event.error?.stack,
});
return false; // Don't suppress default browser error handling
});
// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled rejection:', event.reason);
reportToMonitoring({
message: String(event.reason),
stack: event.reason?.stack,
type: 'unhandledrejection',
});
event.preventDefault(); // Suppress default browser warning (optional)
});
Error Boundaries in React
For React apps, use error boundaries to prevent the whole app from unmounting:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
reportToMonitoring({ error, componentStack: info.componentStack });
}
render() {
if (this.state.hasError) {
return this.props.fallback || <div>Something went wrong.</div>;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<ErrorPage />}>
<App />
</ErrorBoundary>
Error Stack Traces
Stack traces tell you where the error originated:
function outer() {
inner();
}
function inner() {
throw new Error('Error in inner');
}
outer();
// Error: Error in inner
// at inner (app.js:7)
// at outer (app.js:3)
// at <anonymous> (app.js:10)
Firefox DevTools shows this in the Console. Click any filename in the stack trace to jump to that line in the Debugger.
Preserve stack traces across async boundaries:
// Wrapping an error: attach the cause
async function fetchUser(id) {
try {
return await fetch(`/api/user/${id}`).then(r => r.json());
} catch (cause) {
throw new Error(`Failed to fetch user ${id}`, { cause }); // ES2022
}
}
// The cause property lets you trace the original error
try {
await fetchUser(123);
} catch (error) {
console.error(error.message); // 'Failed to fetch user 123'
console.error(error.cause); // Original fetch error
}
Retry with Error Classification
Not all errors are worth retrying:
function isRetryable(error) {
if (error instanceof NetworkError) {
return error.status >= 500 || error.status === 429; // Server errors, rate limit
}
if (error.name === 'AbortError') return false; // User cancelled
if (error instanceof TypeError) return false; // Programming error
return true; // Default: retry
}
async function withRetry(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts || !isRetryable(error)) throw error;
await new Promise(r => setTimeout(r, 1000 * attempt)); // Backoff
}
}
}
Firefox DevTools: Error Debugging
Firefox DevTools Console:
- Errors appear in red with the message and file:line
- Click the file link to jump to the Debugger at that exact line
- Pause on exceptions: Debugger → click the pause-on-exception button (stop sign icon)
- Firefox pauses execution exactly when the error is thrown — inspect local variables before the stack unwinds
The "Pause on caught exceptions" option pauses even inside try/catch blocks.
Top comments (0)