Error Handling Deep Dive
Errors in Production
Unhandled errors crash user flows, leak stack traces, and erode trust. Production JavaScript requires deliberate error handling at every layer — synchronous code, Promises, async/await, event handlers, and third-party scripts.
Error Types
JavaScript provides built-in error constructors:
| Type | When thrown |
|---|---|
Error |
Generic errors |
SyntaxError |
Invalid syntax (usually parse time) |
ReferenceError |
Undefined variable |
TypeError |
Wrong type operation |
RangeError |
Value out of range |
URIError |
Invalid URI handling |
try {
JSON.parse('invalid json');
} catch (err) {
console.log(err instanceof SyntaxError); // true
console.log(err.message);
console.log(err.stack);
}
Custom Errors
Create domain-specific errors for clearer handling:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
}
}
function validateEmail(email) {
if (!email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
}
try {
validateEmail('bad');
} catch (err) {
if (err instanceof ValidationError) {
showFieldError(err.field, err.message);
} else {
throw err;
}
}
Error Cause (ES2022)
Chain errors to preserve context:
try {
await fetchUser();
} catch (err) {
throw new Error('Failed to load dashboard', { cause: err });
}
// Access chain
catch (err) {
console.log(err.message); // 'Failed to load dashboard'
console.log(err.cause.message); // original error
}
Try/Catch Best Practices
try {
riskyOperation();
} catch (err) {
logError(err);
showUserMessage('Something went wrong. Please try again.');
} finally {
hideLoadingSpinner(); // always runs
}
- Catch specific errors when possible
- Don’t swallow errors silently — log or re-throw
- Use
finallyfor cleanup (close connections, hide spinners)
Async Error Handling
Promises
fetch('/api/data')
.then(r => {
if (!r.ok) throw new NetworkError('Request failed', r.status);
return r.json();
})
.catch(err => handleError(err));
// Unhandled rejection — always attach catch
async function load() {
try {
await fetchData();
} catch (err) {
handleError(err);
}
}
Global Unhandled Rejection
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
event.preventDefault(); // prevent default console error
reportToErrorService(event.reason);
});
In Node.js: process.on('unhandledRejection', ...).
Global Error Handlers
window.onerror = (message, source, lineno, colno, error) => {
reportToErrorService({ message, source, lineno, colno, stack: error?.stack });
return true; // prevent default browser error display (optional)
};
window.addEventListener('error', (event) => {
if (event.target instanceof HTMLScriptElement) {
console.error('Script failed to load:', event.target.src);
}
}, true); // capture phase for resource errors
Error Boundaries Pattern (Concept)
In React, Error Boundaries catch render errors. In vanilla JS, wrap component init:
function safeInit(InitFn) {
try {
InitFn();
} catch (err) {
console.error('Init failed:', err);
document.body.innerHTML = '<p>Failed to load application.</p>';
}
}
Result Type Pattern
Avoid exceptions for expected failures:
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return { ok: false, error: `HTTP ${response.status}` };
}
const data = await response.json();
return { ok: true, data };
} catch (err) {
return { ok: false, error: err.message };
}
}
const result = await fetchUser(1);
if (result.ok) {
renderUser(result.data);
} else {
showError(result.error);
}
Logging and Monitoring
function reportError(error, context = {}) {
const payload = {
message: error.message,
stack: error.stack,
name: error.name,
cause: error.cause?.message,
url: location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
...context
};
// Send to Sentry, Datadog, etc.
if (window.Sentry) {
Sentry.captureException(error, { extra: context });
}
}
Never log passwords, tokens, or PII.
User-Facing Error Messages
| Internal | User sees |
|---|---|
ECONNREFUSED |
“Unable to connect. Check your internet.” |
ValidationError: email |
“Please enter a valid email address.” |
HTTP 500 |
“Something went wrong on our end. Try again later.” |
Map technical errors to actionable user messages.
Assertions (Development)
function assert(condition, message) {
if (!condition) {
throw new Error(`Assertion failed: ${message}`);
}
}
// Node 18+: import assert from 'node:assert/strict';
Remove or disable assertions in production builds.
Debugging Strategies
- Reproduce minimally — isolate failing code path
- Read stack trace bottom-up — find origin
- Log structured context — inputs, state, user actions before error
- Use breakpoints — conditional breaks on error paths
- Binary search — comment out half the code to locate issue
Best Practices
- Fail fast — validate inputs early
- Centralize error reporting — one
reportErrorfunction - Never expose stack traces to users in production
- Handle async errors — try/catch with await, .catch on chains
- Test error paths — not just happy paths
Troubleshooting
Error swallowed with empty catch
- Log or re-throw; empty
catch {}hides bugs
Stack trace unhelpful (minified)
- Use source maps in production error reporting
Intermittent async errors
- Race conditions — add logging with timestamps
Robust error handling separates prototype code from production-ready applications.