JavaScript Try-Catch: Complete Error Handling Guide
Master JavaScript error handling with try-catch blocks. Learn about throwing errors, catching exceptions, finally blocks, and best practices for robust error management.
Error handling is crucial for building robust JavaScript applications. The try-catch statement allows you to handle runtime errors gracefully, preventing your application from crashing and providing better user experience.
Basic Try-Catch Syntax
The try-catch statement consists of a try block followed by a catch block. Code that might throw an error goes in the try block, and error handling code goes in the catch block.
Simple Try-Catch
try {
// Code that might throw an error
let result = someRiskyOperation();
console.log(result);
} catch (error) {
// Handle the error
console.error('An error occurred:', error.message);
}
// Basic example
try {
let data = JSON.parse('invalid json');
} catch (error) {
console.log('Failed to parse JSON:', error.message);
// Output: Failed to parse JSON: Unexpected token i in JSON at position 0
}
// Multiple statements in try block
try {
console.log('Starting operation...');
let user = getUser(); // Might throw
let profile = getProfile(user.id); // Might throw
console.log('Operation completed');
} catch (error) {
console.error('Operation failed:', error);
}
The Error Object
try {
nonExistentFunction();
} catch (error) {
// Error object properties
console.log('Name:', error.name); // "ReferenceError"
console.log('Message:', error.message); // "nonExistentFunction is not defined"
console.log('Stack:', error.stack); // Full stack trace
// Standard error types
// - Error: Generic error
// - SyntaxError: Syntax mistakes
// - ReferenceError: Invalid reference
// - TypeError: Wrong type
// - RangeError: Number out of range
// - URIError: URI handling errors
}
// Accessing error properties safely
try {
someOperation();
} catch (error) {
const errorInfo = {
name: error?.name || 'Unknown Error',
message: error?.message || 'An error occurred',
stack: error?.stack || 'No stack trace available',
timestamp: new Date().toISOString(),
};
console.error(errorInfo);
}
Throwing Errors
You can throw your own errors using the throw
statement.
Basic Throw
// Throwing strings (not recommended)
try {
throw 'Something went wrong';
} catch (error) {
console.log(error); // "Something went wrong"
}
// Throwing Error objects (recommended)
try {
throw new Error('Something went wrong');
} catch (error) {
console.log(error.name); // "Error"
console.log(error.message); // "Something went wrong"
console.log(error.stack); // Stack trace
}
// Conditional throwing
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero is not allowed');
}
return a / b;
}
try {
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // Throws error
} catch (error) {
console.error('Math error:', error.message);
}
Custom Error Types
// Basic custom error
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
}
}
// Using custom errors
function validateEmail(email) {
if (!email) {
throw new ValidationError('Email is required');
}
if (!email.includes('@')) {
throw new ValidationError('Invalid email format');
}
return true;
}
try {
validateEmail('invalid-email');
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation failed:', error.message);
} else {
console.log('Unknown error:', error);
}
}
// More complex custom error
class APIError extends Error {
constructor(message, code, statusCode, endpoint) {
super(message);
this.name = 'APIError';
this.code = code;
this.statusCode = statusCode;
this.endpoint = endpoint;
this.timestamp = new Date();
}
toJSON() {
return {
name: this.name,
message: this.message,
code: this.code,
statusCode: this.statusCode,
endpoint: this.endpoint,
timestamp: this.timestamp,
};
}
}
// Usage
try {
throw new APIError('User not found', 'USER_NOT_FOUND', 404, '/api/users/123');
} catch (error) {
console.log(JSON.stringify(error, null, 2));
}
The Finally Block
The finally
block executes regardless of whether an error was thrown or caught.
Basic Finally Usage
try {
console.log('Try block');
// Some operation
} catch (error) {
console.log('Catch block');
} finally {
console.log('Finally block - always runs');
}
// Finally runs even with return statements
function testFinally() {
try {
console.log('In try');
return 'try return';
} catch (error) {
console.log('In catch');
return 'catch return';
} finally {
console.log('In finally');
// Note: returning from finally overrides previous returns
}
}
console.log(testFinally());
// Output:
// In try
// In finally
// try return
Resource Cleanup with Finally
// File handling example
function readFile(filename) {
let file;
try {
file = openFile(filename);
const data = file.read();
return processData(data);
} catch (error) {
console.error('Error reading file:', error);
throw error;
} finally {
// Always close the file
if (file) {
file.close();
console.log('File closed');
}
}
}
// Database connection example
class Database {
async query(sql) {
let connection;
try {
connection = await this.getConnection();
const result = await connection.execute(sql);
return result;
} catch (error) {
console.error('Query failed:', error);
throw error;
} finally {
if (connection) {
await connection.release();
console.log('Connection released');
}
}
}
}
// Loading state management
function fetchData() {
setLoading(true);
try {
const data = performFetch();
processData(data);
} catch (error) {
showError(error.message);
} finally {
setLoading(false); // Always hide loader
}
}
Nested Try-Catch
Try-catch blocks can be nested to handle errors at different levels.
// Nested error handling
try {
console.log('Outer try');
try {
console.log('Inner try');
throw new Error('Inner error');
} catch (innerError) {
console.log('Inner catch:', innerError.message);
throw new Error('Processed inner error');
}
} catch (outerError) {
console.log('Outer catch:', outerError.message);
}
// Real-world example
function processUserData(userId) {
try {
const user = getUser(userId);
try {
const profile = enrichProfile(user);
return profile;
} catch (profileError) {
console.warn('Could not enrich profile:', profileError);
// Return basic user data as fallback
return user;
}
} catch (userError) {
console.error('Failed to get user:', userError);
throw new Error('User processing failed');
}
}
// Multiple operation handling
function complexOperation() {
const results = [];
try {
// First operation
try {
results.push(operation1());
} catch (error) {
console.warn('Operation 1 failed:', error);
results.push(null);
}
// Second operation
try {
results.push(operation2());
} catch (error) {
console.warn('Operation 2 failed:', error);
results.push(null);
}
// Third operation (critical)
try {
results.push(operation3());
} catch (error) {
console.error('Critical operation 3 failed:', error);
throw error; // Re-throw critical errors
}
return results;
} catch (error) {
console.error('Complex operation failed:', error);
throw new Error('Unable to complete operation');
}
}
Async Error Handling
Try-Catch with Async/Await
// Basic async error handling
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('Failed to fetch user:', error);
throw error;
}
}
// Multiple async operations
async function processMultipleUsers(userIds) {
const results = [];
for (const id of userIds) {
try {
const user = await fetchUser(id);
results.push({ id, user, status: 'success' });
} catch (error) {
results.push({ id, error: error.message, status: 'failed' });
}
}
return results;
}
// Parallel async error handling
async function fetchAllData() {
try {
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
fetch('/api/comments').then((r) => r.json()),
]);
return { users, posts, comments };
} catch (error) {
console.error('Failed to fetch all data:', error);
// Try fallback
return { users: [], posts: [], comments: [] };
}
}
Promise Error Handling
// Promise with catch
fetchData()
.then((data) => processData(data))
.then((result) => displayResult(result))
.catch((error) => {
console.error('Promise chain failed:', error);
})
.finally(() => {
console.log('Cleanup');
});
// Converting callback errors to promises
function readFilePromise(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
// Async error propagation
async function getData() {
try {
const data = await readFilePromise('data.json');
return JSON.parse(data);
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error('Data file not found');
} else if (error instanceof SyntaxError) {
throw new Error('Invalid JSON in data file');
} else {
throw error;
}
}
}
Error Handling Patterns
Error Recovery
// Retry pattern
async function fetchWithRetry(url, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
lastError = error;
console.log(`Attempt ${i + 1} failed:`, error.message);
if (i < maxRetries - 1) {
// Wait before retry (exponential backoff)
await new Promise((resolve) =>
setTimeout(resolve, 1000 * Math.pow(2, i))
);
}
}
}
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}
// Fallback values
function getConfig(key) {
try {
const config = JSON.parse(localStorage.getItem('config'));
return config[key];
} catch (error) {
console.warn('Failed to get config:', error);
// Return default values
const defaults = {
theme: 'light',
language: 'en',
pageSize: 20,
};
return defaults[key];
}
}
// Circuit breaker pattern
class CircuitBreaker {
constructor(fn, threshold = 5, timeout = 60000) {
this.fn = fn;
this.threshold = threshold;
this.timeout = timeout;
this.failures = 0;
this.nextAttempt = Date.now();
}
async call(...args) {
if (this.failures >= this.threshold) {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
// Reset after timeout
this.failures = 0;
}
try {
const result = await this.fn(...args);
this.failures = 0; // Reset on success
return result;
} catch (error) {
this.failures++;
if (this.failures >= this.threshold) {
this.nextAttempt = Date.now() + this.timeout;
}
throw error;
}
}
}
Error Aggregation
// Collecting multiple errors
class ErrorCollector {
constructor() {
this.errors = [];
}
add(error, context) {
this.errors.push({
error,
context,
timestamp: new Date(),
});
}
hasErrors() {
return this.errors.length > 0;
}
clear() {
this.errors = [];
}
getReport() {
return this.errors.map((item) => ({
message: item.error.message,
type: item.error.name,
context: item.context,
timestamp: item.timestamp.toISOString(),
}));
}
}
// Usage
const collector = new ErrorCollector();
function validateForm(formData) {
if (!formData.email) {
collector.add(new Error('Email is required'), 'email');
}
if (!formData.password) {
collector.add(new Error('Password is required'), 'password');
}
if (formData.password && formData.password.length < 8) {
collector.add(new Error('Password too short'), 'password');
}
if (collector.hasErrors()) {
const report = collector.getReport();
throw new ValidationError('Form validation failed', report);
}
}
Global Error Handling
// Browser global error handler
window.addEventListener('error', (event) => {
console.error('Global error:', {
message: event.message,
source: event.filename,
line: event.lineno,
column: event.colno,
error: event.error,
});
// Send to error tracking service
trackError(event.error);
});
// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
// Prevent default browser behavior
event.preventDefault();
// Handle the error
handleError(event.reason);
});
// Node.js global error handlers
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
// Graceful shutdown
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
});
Best Practices
1. Be Specific with Error Handling
// Bad - catching all errors the same way
try {
performOperation();
} catch (error) {
console.log('Error occurred');
}
// Good - specific error handling
try {
performOperation();
} catch (error) {
if (error instanceof TypeError) {
console.error('Type error:', error.message);
// Handle type error
} else if (error instanceof NetworkError) {
console.error('Network error:', error.message);
// Retry or show offline message
} else {
console.error('Unexpected error:', error);
// Generic error handling
}
}
2. Don't Suppress Errors Silently
// Bad - swallowing errors
try {
riskyOperation();
} catch (error) {
// Silent fail - don't do this!
}
// Good - at least log the error
try {
riskyOperation();
} catch (error) {
console.error('Operation failed:', error);
// Optionally re-throw or handle appropriately
}
3. Use Error Boundaries (React)
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error boundary caught:', error, errorInfo);
// Log to error reporting service
}
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
4. Provide Meaningful Error Messages
// Bad - generic messages
throw new Error('Error');
throw new Error('Invalid input');
// Good - specific, actionable messages
throw new Error('Email address must contain @ symbol');
throw new Error(`User ID ${userId} not found in database`);
throw new Error(`API rate limit exceeded. Try again in ${retryAfter} seconds`);
5. Error Logging and Monitoring
class ErrorLogger {
static log(error, severity = 'error', context = {}) {
const errorData = {
message: error.message,
stack: error.stack,
severity,
context,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
};
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.error('Error logged:', errorData);
}
// Send to error tracking service
if (process.env.NODE_ENV === 'production') {
sendToErrorService(errorData);
}
}
}
// Usage
try {
performOperation();
} catch (error) {
ErrorLogger.log(error, 'critical', {
operation: 'performOperation',
userId: getCurrentUserId(),
});
}
Conclusion
Error handling with try-catch is essential for building robust JavaScript applications. By properly catching and handling errors, you can prevent application crashes, provide better user experience, and make debugging easier. Remember to be specific with error types, always handle errors appropriately, use finally blocks for cleanup, and implement proper error logging and monitoring. With modern async/await syntax, error handling has become more intuitive, but the fundamental principles remain the same: anticipate failures, handle them gracefully, and always provide meaningful feedback.