What is Error Handling?
Error handling is the practice of anticipating, detecting, and gracefully managing runtime errors in your JavaScript applications. Instead of letting an unexpected exception crash the entire program or leave the user staring at a frozen screen, error handling provides a structured way to capture failure states, log diagnostic information, and decide on fallback behavior.
Why It Matters
Without proper error handling, a single thrown exception can break your entire application, corrupt shared state, or leak sensitive information through stack traces in production. In server-side Node.js applications, uncaught exceptions may crash the process, causing downtime. In browser-based apps, an unhandled error can stop further JavaScript execution, breaking interactivity. Robust error handling patterns ensure:
- Reliability – the application remains operational even when parts fail.
- Security – internal details are not exposed to the client.
- Debuggability – meaningful logs help developers trace root causes.
- User experience – users receive clear feedback instead of a broken interface.
Core Error Handling Mechanisms
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Try…Catch…Finally
The fundamental block for synchronous error handling. Wrap code that may throw in try, handle the error in catch, and execute cleanup logic in finally.
function parseJson(input) {
try {
const result = JSON.parse(input);
console.log('Parsed successfully:', result);
return result;
} catch (error) {
console.error('Parse failed:', error.message);
// Provide a fallback or rethrow
return null;
} finally {
console.log('Cleanup: releasing resources if needed');
}
}
parseJson('{ "valid": true }'); // works
parseJson('invalid string'); // caught, returns null
Throwing Custom Errors
Throwing your own errors allows you to attach domain-specific information and differentiate error types. Use the throw keyword with built-in error constructors (Error, TypeError, RangeError) or custom classes.
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.code = 400;
}
}
function validateEmail(email) {
if (!email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
return true;
}
try {
validateEmail('user-at-example');
} catch (err) {
if (err instanceof ValidationError) {
console.warn(`Validation failed on ${err.field}: ${err.message}`);
} else {
console.error('Unexpected error:', err);
}
}
Custom error classes enable selective catching based on instanceof checks, keeping error handling precise.
Asynchronous Error Handling Patterns
Promise Chains with .catch()
With Promise-based APIs, errors are caught using the .catch() method. If any .then() callback throws or a promise rejects, control flows to the nearest .catch().
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('Data:', data);
})
.catch(error => {
console.error('Fetch failed:', error.message);
// Optional: display user-friendly message
})
.finally(() => {
console.log('Request finished.');
});
Async/Await with Try…Catch
async/await makes asynchronous code look synchronous, and you can wrap it with traditional try/catch. This improves readability and allows handling errors at the call site.
async function loadUserProfile(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error(`Status ${response.status}`);
const profile = await response.json();
return profile;
} catch (error) {
console.error('Failed to load profile:', error);
// Decide whether to rethrow or return a default
return { name: 'Guest', email: null };
} finally {
console.log('Profile fetch attempt completed');
}
}
// Usage
loadUserProfile(42).then(profile => console.log(profile));
Handling Multiple Async Operations
When dealing with several independent asynchronous tasks, you can use Promise.allSettled() to avoid a single rejection aborting the entire group.
const promises = [
fetch('/api/orders').then(r => r.json()),
fetch('/api/inventory').then(r => r.json()),
Promise.reject(new Error('Simulated failure'))
];
const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} succeeded:`, result.value);
} else {
console.error(`Promise ${index} failed:`, result.reason);
}
});
Error Handling in Event Listeners and Timers
DOM Event Handlers
Errors thrown inside event handlers do not propagate to an outer try/catch because event dispatch happens later. Wrap handler bodies individually.
document.querySelector('#submit-btn').addEventListener('click', () => {
try {
// Complex logic that may throw
const input = document.querySelector('#username').value;
if (!input) throw new Error('Username required');
submitForm(input);
} catch (error) {
console.error('Submit handler failed:', error);
// Show inline error message instead of breaking the page
}
});
setTimeout / setInterval
Similarly, callbacks scheduled with setTimeout or setInterval run outside the current execution context, so wrap them internally.
setTimeout(() => {
try {
riskyOperation();
} catch (e) {
console.error('Scheduled operation failed:', e);
}
}, 1000);
Global Error Handlers
For errors that slip through local handling, JavaScript provides global hooks. These are essential as a last-resort safety net, especially in production.
Browser: window.onerror & unhandledrejection
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global catch:', message, 'at', source, lineno);
// Optionally send to analytics service
return true; // Prevent default browser logging
};
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled promise rejection:', event.reason);
// Prevent default (some browsers show an error overlay)
event.preventDefault();
});
Node.js: process Events
process.on('uncaughtException', (error) => {
console.error('Fatal uncaught exception:', error);
// Perform cleanup then exit
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
// Application can continue, but it's a sign of a bug
});
Advanced Error Handling Patterns
Error Boundary Pattern (Functional)
Inspired by React Error Boundaries, you can create higher-order functions that wrap any function and catch errors, returning a fallback value or logging.
function withErrorBoundary(fn, fallbackValue) {
return function(...args) {
try {
return fn.apply(this, args);
} catch (error) {
console.error(`Error in ${fn.name || 'anonymous'}:`, error);
return fallbackValue;
}
};
}
const riskyCalculation = withErrorBoundary(
(input) => complexMath(input),
null
);
const result = riskyCalculation(5); // returns null on error instead of crashing
Result / Either Pattern
Instead of throwing, some functions return a discriminated union (often called "Result") that explicitly indicates success or failure. This forces the caller to handle both cases.
function safeParseJson(input) {
try {
const data = JSON.parse(input);
return { ok: true, value: data };
} catch (error) {
return { ok: false, error: error.message };
}
}
const result = safeParseJson(userInput);
if (result.ok) {
console.log('Parsed:', result.value);
} else {
console.error('Parse error:', result.error);
}
This pattern eliminates hidden exceptions and makes error handling explicit and predictable.
Centralized Error Handler in Express.js
In Node.js web applications, a centralized error-handling middleware catches errors from route handlers and sends standardized responses.
// Route handler with async error wrapper
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get('/data', asyncHandler(async (req, res) => {
const data = await fetchExternalData();
res.json(data);
}));
// Centralized error middleware (must have 4 parameters)
app.use((err, req, res, next) => {
console.error('Request error:', err.stack);
const status = err.status || 500;
res.status(status).json({
error: {
message: err.message,
status
}
});
});
Best Practices
- Always handle errors locally where possible. Catch at the point where you can meaningfully recover or provide feedback.
- Distinguish error types. Use
instanceofor custom error properties to differentiate validation errors, network errors, etc. - Never swallow errors silently. At minimum, log the error. A bare
catch {}makes debugging nearly impossible. - Avoid throwing in finally blocks. If
finallythrows, it overrides any previous exception, potentially masking the original error. - Use async wrappers. For Express or similar frameworks, wrap async route handlers to propagate rejections to error middleware.
- Set up global handlers as a safety net.
window.onerrorandunhandledrejectionin browsers, oruncaughtExceptionin Node.js, catch truly unexpected failures. - Prefer explicit error results for critical logic. The Result pattern reduces surprise and encourages compile-time-like safety.
- Include meaningful error messages and codes. Help the consumer understand what went wrong, without exposing internals.
- Test error paths. Write unit tests that intentionally trigger errors to verify your handling logic works as expected.
- Clean up resources in
finally. Close files, clear timers, or release locks regardless of success or failure.
Conclusion
Error handling in JavaScript is not just about preventing crashes — it’s a fundamental design discipline that shapes the robustness, maintainability, and user experience of your applications. By combining synchronous try/catch blocks, custom error classes, asynchronous patterns like .catch() and async-aware wrappers, global fallback handlers, and explicit result objects, you create a layered defense against runtime failures. Adopting these patterns consistently across your codebase will make your applications more predictable, easier to debug, and resilient in the face of unexpected conditions. Remember, every unhandled error is a bug waiting to surface; every handled error is an opportunity for graceful recovery.