Try-catch is JavaScript's fundamental error handling mechanism that allows developers to gracefully handle runtime errors and prevent application crashes.
In this comprehensive tutorial, we'll explore try-catch statements, error types, best practices, and advanced error handling patterns in JavaScript.
Try-Catch Basics
The try-catch statement consists of a try block that contains code that might throw an error, and a catch block that handles the error if one occurs.
Basic Try-Catch Syntax
// Basic try-catch syntax
try {
// Code that might throw an error
} catch (error) {
// Code to handle the error
}
// Simple example
try {
const result = 10 / 0;
console.log(result); // Infinity (no error)
} catch (error) {
console.error('An error occurred:', error.message);
}
// Example with actual error
try {
const result = undefinedVariable.property; // Throws ReferenceError
} catch (error) {
console.error('Error caught:', error.message);
console.error('Error type:', error.name);
}
// Example with JSON parsing
try {
const invalidJson = '{ "name": "John", age: 30 }'; // Invalid JSON
const parsed = JSON.parse(invalidJson);
console.log(parsed);
} catch (error) {
console.error('JSON parsing error:', error.message);
}
// Example with function that throws error
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero is not allowed');
}
return a / b;
}
try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error('Division error:', error.message);
}
// Try-catch with return statement
function safeDivide(a, b) {
try {
return a / b;
} catch (error) {
console.error('Division failed:', error.message);
return null; // Return null on error
}
}
const result1 = safeDivide(10, 2);
const result2 = safeDivide(10, 0);
console.log(result1); // 5
console.log(result2); // null
Try-Catch-Finally
// Try-catch-finally syntax
try {
// Code that might throw an error
} catch (error) {
// Code to handle the error
} finally {
// Code that always executes
}
// Example with finally block
function processFile(filename) {
let fileContent = null;
try {
// Simulate file reading
if (filename === 'nonexistent.txt') {
throw new Error('File not found');
}
fileContent = 'File content here';
console.log('File processed successfully');
return fileContent;
} catch (error) {
console.error('File processing error:', error.message);
return null;
} finally {
console.log('Cleanup: Closing file resources');
// This always executes, regardless of error
}
}
processFile('data.txt'); // Success case
processFile('nonexistent.txt'); // Error case
// Finally block executes even with return
function testFinally() {
try {
console.log('In try block');
return 'Return from try';
} catch (error) {
console.log('In catch block');
return 'Return from catch';
} finally {
console.log('In finally block');
// This executes before return
}
}
const result = testFinally();
console.log(result);
// Finally block with throw
function testFinallyWithThrow() {
try {
console.log('In try block');
throw new Error('Something went wrong');
} catch (error) {
console.log('In catch block:', error.message);
return 'Handled error';
} finally {
console.log('In finally block');
// Still executes even though catch handled the error
}
}
const result2 = testFinallyWithThrow();
console.log(result2);
// Finally without catch
function tryFinallyOnly() {
try {
console.log('In try block');
// Some operation that might throw
return 'Success';
} finally {
console.log('In finally block');
// Cleanup code here
}
}
const result3 = tryFinallyOnly();
console.log(result3);
Error Object Properties
// Error object properties
try {
throw new Error('Custom error message');
} catch (error) {
console.log('Error name:', error.name); // 'Error'
console.log('Error message:', error.message); // 'Custom error message'
console.log('Error stack:', error.stack); // Stack trace
console.log('Error toString():', error.toString()); // 'Error: Custom error message'
}
// Different error types
try {
// ReferenceError
const result = undefinedVariable.property;
} catch (error) {
console.log('Error type:', error.constructor.name); // 'ReferenceError'
console.log('Error name:', error.name); // 'ReferenceError'
console.log('Error message:', error.message);
}
try {
// TypeError
const obj = null;
const result = obj.property;
} catch (error) {
console.log('Error type:', error.constructor.name); // 'TypeError'
console.log('Error name:', error.name); // 'TypeError'
console.log('Error message:', error.message);
}
try {
// SyntaxError
// eval('const x = ;'); // This would throw SyntaxError
throw new SyntaxError('Invalid syntax');
} catch (error) {
console.log('Error type:', error.constructor.name); // 'SyntaxError'
console.log('Error name:', error.name); // 'SyntaxError'
console.log('Error message:', error.message);
}
try {
// RangeError
const arr = new Array(-1); // Invalid array length
} catch (error) {
console.log('Error type:', error.constructor.name); // 'RangeError'
console.log('Error name:', error.name); // 'RangeError'
console.log('Error message:', error.message);
}
// Custom error properties
class CustomError extends Error {
constructor(message, code, details) {
super(message);
this.name = 'CustomError';
this.code = code;
this.details = details;
this.timestamp = new Date();
}
}
try {
throw new CustomError('Something went wrong', 'ERR_001', { userId: 123 });
} catch (error) {
console.log('Error name:', error.name); // 'CustomError'
console.log('Error message:', error.message); // 'Something went wrong'
console.log('Error code:', error.code); // 'ERR_001'
console.log('Error details:', error.details); // { userId: 123 }
console.log('Error timestamp:', error.timestamp);
}
Error Types
JavaScript has several built-in error types that represent different kinds of runtime errors.
Built-in Error Types
// Error - Base error class
try {
throw new Error('Generic error');
} catch (error) {
console.log('Error:', error.name, error.message);
}
// ReferenceError - When trying to access undefined variable
try {
console.log(undefinedVariable);
} catch (error) {
console.log('ReferenceError:', error.message);
}
// TypeError - When operation is performed on wrong type
try {
const num = 42;
num(); // Trying to call number as function
} catch (error) {
console.log('TypeError:', error.message);
}
// SyntaxError - When parsing invalid JavaScript code
try {
eval('const x = ;'); // Invalid syntax
} catch (error) {
console.log('SyntaxError:', error.message);
}
// RangeError - When numeric value is outside allowed range
try {
const arr = new Array(Number.MAX_SAFE_INTEGER);
} catch (error) {
console.log('RangeError:', error.message);
}
// URIError - When URI manipulation functions are used incorrectly
try {
decodeURIComponent('%'); // Invalid URI component
} catch (error) {
console.log('URIError:', error.message);
}
// EvalError - Historically used for eval-related errors (rarely used now)
try {
throw new EvalError('Eval error example');
} catch (error) {
console.log('EvalError:', error.message);
}
// AggregateError - Multiple errors in one (ES2021)
try {
const errors = [
new Error('First error'),
new Error('Second error'),
new Error('Third error')
];
throw new AggregateError(errors, 'Multiple errors occurred');
} catch (error) {
console.log('AggregateError:', error.message);
console.log('Individual errors:');
error.errors.forEach((err, index) => {
console.log(` ${index + 1}. ${err.message}`);
});
}
// InternalError - Indicates an error in the JavaScript engine (rare)
try {
throw new InternalError('Internal error example');
} catch (error) {
console.log('InternalError:', error.message);
}
Custom Error Classes
// Creating custom error classes
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class AuthenticationError extends Error {
constructor(message, userId) {
super(message);
this.name = 'AuthenticationError';
this.userId = userId;
}
}
class NetworkError extends Error {
constructor(message, statusCode, url) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
this.url = url;
}
}
class DatabaseError extends Error {
constructor(message, query, params) {
super(message);
this.name = 'DatabaseError';
this.query = query;
this.params = params;
}
}
// Using custom errors
function validateEmail(email) {
if (!email) {
throw new ValidationError('Email is required', 'email');
}
if (!email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
return true;
}
function authenticateUser(username, password) {
if (!username || !password) {
throw new AuthenticationError('Username and password required', username);
}
if (username !== 'admin' || password !== 'secret') {
throw new AuthenticationError('Invalid credentials', username);
}
return { id: 1, name: 'Admin User' };
}
function makeApiRequest(url) {
if (url === 'https://api.example.com/error') {
throw new NetworkError('API request failed', 500, url);
}
return { data: 'success' };
}
function executeQuery(query, params) {
if (query.includes('DROP')) {
throw new DatabaseError('Dangerous query detected', query, params);
}
return { rows: [] };
}
// Testing custom errors
try {
validateEmail('invalid-email');
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Validation failed for field '${error.field}': ${error.message}`);
}
}
try {
authenticateUser('user', 'wrong');
} catch (error) {
if (error instanceof AuthenticationError) {
console.log(`Authentication failed for user '${error.userId}': ${error.message}`);
}
}
try {
makeApiRequest('https://api.example.com/error');
} catch (error) {
if (error instanceof NetworkError) {
console.log(`Network error: ${error.message} (Status: ${error.statusCode}, URL: ${error.url})`);
}
}
try {
executeQuery('DROP TABLE users', []);
} catch (error) {
if (error instanceof DatabaseError) {
console.log(`Database error: ${error.message} (Query: ${error.query})`);
}
}
// Error hierarchy and instanceof checking
class AppError extends Error {
constructor(message, code) {
super(message);
this.name = 'AppError';
this.code = code;
}
}
class UserError extends AppError {
constructor(message, userId) {
super(message, 'USER_ERROR');
this.userId = userId;
}
}
class SystemError extends AppError {
constructor(message, component) {
super(message, 'SYSTEM_ERROR');
this.component = component;
}
}
function handleError(error) {
if (error instanceof UserError) {
console.log(`User error: ${error.message} (User: ${error.userId})`);
} else if (error instanceof SystemError) {
console.log(`System error: ${error.message} (Component: ${error.component})`);
} else if (error instanceof AppError) {
console.log(`App error: ${error.message} (Code: ${error.code})`);
} else {
console.log(`Unknown error: ${error.message}`);
}
}
try {
throw new UserError('Invalid user input', 123);
} catch (error) {
handleError(error);
}
Error Handling Patterns
// Pattern 1: Early error detection
function processData(data) {
if (!data) {
throw new Error('Data is required');
}
if (!Array.isArray(data)) {
throw new TypeError('Data must be an array');
}
if (data.length === 0) {
throw new Error('Data array cannot be empty');
}
return data.map(item => item * 2);
}
// Pattern 2: Error wrapping
function safeOperation(operation) {
try {
return operation();
} catch (error) {
throw new Error(`Operation failed: ${error.message}`);
}
}
// Pattern 3: Error aggregation
function validateObject(obj, schema) {
const errors = [];
for (const [field, rules] of Object.entries(schema)) {
const value = obj[field];
if (rules.required && (value === undefined || value === null)) {
errors.push(`${field} is required`);
}
if (rules.type && typeof value !== rules.type) {
errors.push(`${field} must be of type ${rules.type}`);
}
if (rules.min && value < rules.min) {
errors.push(`${field} must be at least ${rules.min}`);
}
if (rules.max && value > rules.max) {
errors.push(`${field} must be at most ${rules.max}`);
}
}
if (errors.length > 0) {
throw new Error(`Validation failed: ${errors.join(', ')}`);
}
return true;
}
// Pattern 4: Retry mechanism
async function retryOperation(operation, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
console.log(`Attempt ${attempt} failed: ${error.message}`);
if (attempt === maxRetries) {
throw new Error(`Operation failed after ${maxRetries} attempts: ${error.message}`);
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
// Pattern 5: Fallback values
function safeParseJSON(jsonString, fallback = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.warn('JSON parsing failed:', error.message);
return fallback;
}
}
// Pattern 6: Error logging
function logError(error, context = {}) {
const errorInfo = {
message: error.message,
name: error.name,
stack: error.stack,
timestamp: new Date().toISOString(),
context
};
console.error('Error logged:', errorInfo);
// In production, send to logging service
// loggingService.logError(errorInfo);
}
// Pattern 7: Error boundaries (React-like concept)
class ErrorHandler {
constructor() {
this.errors = [];
}
handle(error, context = {}) {
this.errors.push({
error,
context,
timestamp: new Date()
});
logError(error, context);
}
getErrors() {
return this.errors;
}
clearErrors() {
this.errors = [];
}
}
const errorHandler = new ErrorHandler();
function riskyOperation(id) {
if (id < 0) {
throw new Error('ID cannot be negative');
}
return `Processed ${id}`;
}
try {
riskyOperation(-1);
} catch (error) {
errorHandler.handle(error, { operation: 'riskyOperation', id: -1 });
}
Async Error Handling
Error handling with asynchronous operations requires special consideration due to the non-blocking nature of JavaScript.
Try-Catch with Promises
// Try-catch with async/await
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch error:', error.message);
throw error; // Re-throw for caller to handle
}
}
// Using the async function
async function processData() {
try {
const data = await fetchData('https://api.example.com/data');
console.log('Data received:', data);
return data;
} catch (error) {
console.error('Failed to process data:', error.message);
return null; // Return null on error
}
}
// Promise-based error handling
function fetchWithPromise(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('Promise error:', error.message);
throw error;
});
}
// Using promise-based function
fetchWithPromise('https://api.example.com/data')
.then(data => console.log('Data:', data))
.catch(error => console.error('Final error handler:', error.message));
// Multiple async operations
async function processMultipleData() {
const results = [];
try {
const data1 = await fetchData('https://api.example.com/data1');
results.push(data1);
const data2 = await fetchData('https://api.example.com/data2');
results.push(data2);
const data3 = await fetchData('https://api.example.com/data3');
results.push(data3);
return results;
} catch (error) {
console.error('Error in processing multiple data:', error.message);
return results; // Return partial results
}
}
// Parallel async operations with error handling
async function processParallelData() {
const urls = [
'https://api.example.com/data1',
'https://api.example.com/data2',
'https://api.example.com/data3'
];
try {
const promises = urls.map(url => fetchData(url));
const results = await Promise.all(promises);
return results;
} catch (error) {
console.error('Error in parallel processing:', error.message);
// Fallback to sequential processing
const results = [];
for (const url of urls) {
try {
const data = await fetchData(url);
results.push(data);
} catch (singleError) {
console.error(`Failed to fetch ${url}:`, singleError.message);
results.push(null); // Add null for failed request
}
}
return results;
}
}
// Promise.allSettled for handling all results
async function processAllSettled() {
const urls = [
'https://api.example.com/data1',
'https://api.example.com/invalid', // This will fail
'https://api.example.com/data3'
];
const promises = urls.map(url =>
fetchData(url).catch(error => ({ error: error.message }))
);
const results = await Promise.allSettled(promises);
return results.map(result => {
if (result.status === 'fulfilled') {
return result.value;
} else {
return { error: result.reason.message };
}
});
}
// Async error handling with timeout
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeout}ms`);
}
throw error;
}
}
Event-Based Error Handling
// Error handling with event emitters
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(listener => {
try {
listener(...args);
} catch (error) {
console.error(`Error in ${event} listener:`, error.message);
this.emit('error', error);
}
});
}
}
}
const emitter = new EventEmitter();
emitter.on('data', (data) => {
if (data === null) {
throw new Error('Null data received');
}
console.log('Processing data:', data);
});
emitter.on('error', (error) => {
console.error('Event emitter error:', error.message);
});
// Test error handling
emitter.emit('data', 'valid data'); // Works fine
emitter.emit('data', null); // Triggers error handling
// Error handling with streams (Node.js-like)
class StreamReader {
constructor(data) {
this.data = data;
this.position = 0;
this.listeners = {};
}
on(event, listener) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(listener);
}
read() {
try {
if (this.position >= this.data.length) {
this.emit('end');
return;
}
const chunk = this.data[this.position++];
this.emit('data', chunk);
// Simulate async reading
setTimeout(() => this.read(), 10);
} catch (error) {
this.emit('error', error);
}
}
emit(event, ...args) {
if (this.listeners[event]) {
this.listeners[event].forEach(listener => {
listener(...args);
});
}
}
}
const reader = new StreamReader([1, 2, 3, null, 5]);
reader.on('data', (chunk) => {
if (chunk === null) {
throw new Error('Null chunk encountered');
}
console.log('Read chunk:', chunk);
});
reader.on('error', (error) => {
console.error('Stream error:', error.message);
});
reader.on('end', () => {
console.log('Stream ended');
});
reader.read();
// Error handling with callbacks
function fetchDataCallback(url, callback) {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => callback(null, data))
.catch(error => callback(error, null));
}
// Using callback-based function
fetchDataCallback('https://api.example.com/data', (error, data) => {
if (error) {
console.error('Callback error:', error.message);
return;
}
console.log('Callback data:', data);
});
// Error handling with generators
function* dataGenerator() {
try {
yield fetch('https://api.example.com/data1');
yield fetch('https://api.example.com/data2');
yield fetch('https://api.example.com/data3');
} catch (error) {
console.error('Generator error:', error.message);
throw error;
}
}
// Using generator with error handling
async function processGenerator() {
const generator = dataGenerator();
try {
for await (const response of generator) {
const data = await response.json();
console.log('Generator data:', data);
}
} catch (error) {
console.error('Generator processing error:', error.message);
}
}
Advanced Async Patterns
// Circuit breaker pattern
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.threshold = threshold;
this.timeout = timeout;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
}
}
}
// Using circuit breaker
const circuitBreaker = new CircuitBreaker();
async function protectedOperation() {
return await circuitBreaker.execute(async () => {
const response = await fetch('https://api.example.com/data');
return response.json();
});
}
// Retry with exponential backoff
async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === maxRetries) {
throw new Error(`Operation failed after ${maxRetries} attempts: ${error.message}`);
}
const delay = baseDelay * Math.pow(2, attempt - 1);
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// Error handling with async generators
async function* asyncDataGenerator(urls) {
for (const url of urls) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
yield data;
} catch (error) {
console.error(`Failed to fetch ${url}:`, error.message);
yield { error: error.message, url };
}
}
}
// Using async generator with error handling
async function processAsyncGenerator() {
const urls = [
'https://api.example.com/data1',
'https://api.example.com/invalid',
'https://api.example.com/data3'
];
for await (const data of asyncDataGenerator(urls)) {
if (data.error) {
console.log('Error in generator:', data.error);
} else {
console.log('Data from generator:', data);
}
}
}
// Error handling with async iterators
class AsyncErrorCollector {
constructor() {
this.errors = [];
}
async collect(iterable) {
for await (const item of iterable) {
try {
await this.processItem(item);
} catch (error) {
this.errors.push({ item, error: error.message });
}
}
return this.errors;
}
async processItem(item) {
// Simulate processing that might fail
if (Math.random() < 0.3) {
throw new Error(`Failed to process ${item}`);
}
console.log('Processed:', item);
}
}
// Using async error collector
const collector = new AsyncErrorCollector();
const items = [1, 2, 3, 4, 5];
const errors = await collector.collect(items);
console.log('Collected errors:', errors);
Practical Applications
Error handling is crucial in real-world applications for maintaining stability and providing good user experience.
API Error Handling
// Comprehensive API error handling
class ApiClient {
constructor(baseURL, timeout = 10000) {
this.baseURL = baseURL;
this.timeout = timeout;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ApiError(
`HTTP ${response.status}: ${response.statusText}`,
response.status,
errorData
);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new ApiError('Request timeout', 408);
}
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(`Network error: ${error.message}`, 0);
}
}
async get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
async put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
async delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
}
class ApiError extends Error {
constructor(message, statusCode, data = {}) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
this.data = data;
}
}
// Using the API client
const api = new ApiClient('https://api.example.com');
async function fetchUser(userId) {
try {
const user = await api.get(`/users/${userId}`);
return user;
} catch (error) {
if (error instanceof ApiError) {
switch (error.statusCode) {
case 404:
console.log('User not found');
return null;
case 401:
console.log('Authentication required');
throw new Error('Please login to continue');
case 403:
console.log('Access denied');
throw new Error('You do not have permission to access this resource');
case 500:
console.log('Server error, please try again later');
throw new Error('Service temporarily unavailable');
default:
console.log('API error:', error.message);
throw error;
}
} else {
console.log('Network error:', error.message);
throw new Error('Unable to connect to the server');
}
}
}
// API error handling with retry
async function fetchUserWithRetry(userId, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fetchUser(userId);
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
console.log(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
// Batch API operations with error handling
async function fetchMultipleUsers(userIds) {
const results = new Map();
await Promise.allSettled(
userIds.map(async (userId) => {
try {
const user = await fetchUser(userId);
results.set(userId, { status: 'success', data: user });
} catch (error) {
results.set(userId, { status: 'error', error: error.message });
}
})
);
return results;
}
// Usage examples
try {
const user = await fetchUserWithRetry(123);
console.log('User:', user);
} catch (error) {
console.error('Final error:', error.message);
}
const userIds = [1, 2, 3, 999, 5];
const userResults = await fetchMultipleUsers(userIds);
console.log('User results:', userResults);
Form Validation Error Handling
// Comprehensive form validation
class FormValidator {
constructor(rules) {
this.rules = rules;
this.errors = {};
}
validate(data) {
this.errors = {};
for (const [field, fieldRules] of Object.entries(this.rules)) {
const value = data[field];
const fieldErrors = this.validateField(field, value, fieldRules);
if (fieldErrors.length > 0) {
this.errors[field] = fieldErrors;
}
}
return {
isValid: Object.keys(this.errors).length === 0,
errors: this.errors
};
}
validateField(field, value, rules) {
const errors = [];
for (const rule of rules) {
const error = this.applyRule(field, value, rule);
if (error) {
errors.push(error);
}
}
return errors;
}
applyRule(field, value, rule) {
const { type, message, ...params } = rule;
switch (type) {
case 'required':
if (!value || value === '') {
return message || `${field} is required`;
}
break;
case 'email':
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (value && !emailRegex.test(value)) {
return message || `${field} must be a valid email`;
}
break;
case 'minLength':
if (value && value.length < params.min) {
return message || `${field} must be at least ${params.min} characters`;
}
break;
case 'maxLength':
if (value && value.length > params.max) {
return message || `${field} must be at most ${params.max} characters`;
}
break;
case 'pattern':
if (value && !params.regex.test(value)) {
return message || `${field} format is invalid`;
}
break;
case 'custom':
if (params.validator && !params.validator(value)) {
return message || `${field} is invalid`;
}
break;
}
return null;
}
}
// Validation rules
const userFormRules = {
name: [
{ type: 'required', message: 'Name is required' },
{ type: 'minLength', min: 2, message: 'Name must be at least 2 characters' },
{ type: 'maxLength', max: 50, message: 'Name must be at most 50 characters' }
],
email: [
{ type: 'required', message: 'Email is required' },
{ type: 'email', message: 'Please enter a valid email address' }
],
age: [
{ type: 'required', message: 'Age is required' },
{ type: 'custom', validator: (value) => {
const age = parseInt(value);
return !isNaN(age) && age >= 18 && age <= 120;
}, message: 'Age must be between 18 and 120' }
],
password: [
{ type: 'required', message: 'Password is required' },
{ type: 'minLength', min: 8, message: 'Password must be at least 8 characters' },
{ type: 'pattern', regex: /[A-Z]/, message: 'Password must contain at least one uppercase letter' },
{ type: 'pattern', regex: /[0-9]/, message: 'Password must contain at least one number' }
]
};
// Using the validator
function validateUserForm(formData) {
const validator = new FormValidator(userFormRules);
const result = validator.validate(formData);
if (!result.isValid) {
console.log('Form validation errors:');
for (const [field, errors] of Object.entries(result.errors)) {
console.log(`${field}:`, errors);
}
throw new ValidationError('Form validation failed', result.errors);
}
return formData;
}
class ValidationError extends Error {
constructor(message, errors) {
super(message);
this.name = 'ValidationError';
this.errors = errors;
}
}
// Form submission with error handling
async function submitUserForm(formData) {
try {
// Validate form data
const validatedData = validateUserForm(formData);
// Submit to API
const response = await api.post('/users', validatedData);
return response;
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation errors:', error.errors);
return { success: false, errors: error.errors };
} else if (error instanceof ApiError) {
console.log('API error:', error.message);
return { success: false, error: 'Server error occurred' };
} else {
console.log('Unexpected error:', error.message);
return { success: false, error: 'An unexpected error occurred' };
}
}
}
// Real-time validation
class RealTimeValidator {
constructor(rules) {
this.validator = new FormValidator(rules);
this.fieldValidators = {};
}
validateField(field, value) {
const fieldRules = this.rules[field];
if (!fieldRules) return { isValid: true, errors: [] };
const errors = this.validator.validateField(field, value, fieldRules);
return {
isValid: errors.length === 0,
errors
};
}
setupFieldValidation(formElement) {
const fields = formElement.querySelectorAll('input, select, textarea');
fields.forEach(field => {
const fieldName = field.name;
if (!fieldName) return;
field.addEventListener('blur', () => {
const value = field.value;
const result = this.validateField(fieldName, value);
this.showFieldErrors(field, result);
});
field.addEventListener('input', () => {
// Clear errors on input
this.clearFieldErrors(field);
});
});
}
showFieldErrors(field, result) {
this.clearFieldErrors(field);
if (!result.isValid) {
const errorElement = document.createElement('div');
errorElement.className = 'field-error';
errorElement.textContent = result.errors[0]; // Show first error
field.parentNode.appendChild(errorElement);
field.classList.add('error');
}
}
clearFieldErrors(field) {
const errorElement = field.parentNode.querySelector('.field-error');
if (errorElement) {
errorElement.remove();
}
field.classList.remove('error');
}
}
Database Error Handling
// Database error handling patterns
class DatabaseError extends Error {
constructor(message, query, params, originalError) {
super(message);
this.name = 'DatabaseError';
this.query = query;
this.params = params;
this.originalError = originalError;
}
}
class DatabaseConnection {
constructor(config) {
this.config = config;
this.connection = null;
}
async connect() {
try {
// Simulate database connection
if (this.config.invalid) {
throw new Error('Invalid database configuration');
}
this.connection = { connected: true };
console.log('Database connected successfully');
} catch (error) {
throw new DatabaseError(
'Failed to connect to database',
'CONNECT',
this.config,
error
);
}
}
async query(sql, params = []) {
if (!this.connection) {
throw new DatabaseError('Database not connected', sql, params);
}
try {
// Simulate query execution
if (sql.includes('ERROR')) {
throw new Error('Query syntax error');
}
console.log('Executing query:', sql, params);
return { rows: [], affectedRows: 0 };
} catch (error) {
throw new DatabaseError(
'Query execution failed',
sql,
params,
error
);
}
}
async transaction(queries) {
if (!this.connection) {
throw new DatabaseError('Database not connected', 'TRANSACTION', queries);
}
try {
console.log('Starting transaction');
for (const { sql, params } of queries) {
await this.query(sql, params);
}
console.log('Transaction completed successfully');
} catch (error) {
console.log('Transaction failed, rolling back');
throw new DatabaseError(
'Transaction failed',
'TRANSACTION',
queries,
error
);
}
}
async close() {
try {
if (this.connection) {
this.connection = null;
console.log('Database connection closed');
}
} catch (error) {
throw new DatabaseError(
'Failed to close database connection',
'CLOSE',
{},
error
);
}
}
}
// Repository pattern with error handling
class UserRepository {
constructor(db) {
this.db = db;
}
async create(userData) {
try {
const sql = 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)';
const params = [userData.name, userData.email, userData.age];
const result = await this.db.query(sql, params);
return { id: result.insertId, ...userData };
} catch (error) {
if (error instanceof DatabaseError) {
if (error.originalError.message.includes('UNIQUE constraint')) {
throw new Error('User with this email already exists');
}
if (error.originalError.message.includes('NOT NULL')) {
throw new Error('Required field is missing');
}
}
throw error;
}
}
async findById(id) {
try {
const sql = 'SELECT * FROM users WHERE id = ?';
const params = [id];
const result = await this.db.query(sql, params);
return result.rows[0] || null;
} catch (error) {
if (error instanceof DatabaseError) {
console.error('Database error in findById:', error.message);
throw new Error('Failed to fetch user');
}
throw error;
}
}
async update(id, userData) {
try {
const sql = 'UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?';
const params = [userData.name, userData.email, userData.age, id];
const result = await this.db.query(sql, params);
return result.affectedRows > 0;
} catch (error) {
if (error instanceof DatabaseError) {
console.error('Database error in update:', error.message);
throw new Error('Failed to update user');
}
throw error;
}
}
async delete(id) {
try {
const sql = 'DELETE FROM users WHERE id = ?';
const params = [id];
const result = await this.db.query(sql, params);
return result.affectedRows > 0;
} catch (error) {
if (error instanceof DatabaseError) {
console.error('Database error in delete:', error.message);
throw new Error('Failed to delete user');
}
throw error;
}
}
}
// Service layer with comprehensive error handling
class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
async createUser(userData) {
try {
// Validate input
this.validateUserData(userData);
// Create user
const user = await this.userRepository.create(userData);
// Log success
console.log('User created successfully:', user.id);
return user;
} catch (error) {
console.error('Failed to create user:', error.message);
// Categorize and re-throw with appropriate message
if (error.message.includes('already exists')) {
throw new Error('A user with this email already exists');
}
if (error.message.includes('Required field')) {
throw new Error('Please provide all required fields');
}
if (error.message.includes('Failed to fetch')) {
throw new Error('Database error occurred');
}
throw new Error('Failed to create user');
}
}
async getUser(id) {
try {
if (!id || id <= 0) {
throw new Error('Invalid user ID');
}
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error('User not found');
}
return user;
} catch (error) {
console.error('Failed to get user:', error.message);
if (error.message.includes('Invalid user ID')) {
throw error;
}
if (error.message.includes('User not found')) {
throw error;
}
throw new Error('Failed to fetch user');
}
}
validateUserData(userData) {
if (!userData.name || userData.name.trim().length < 2) {
throw new Error('Name must be at least 2 characters');
}
if (!userData.email || !this.isValidEmail(userData.email)) {
throw new Error('Valid email is required');
}
if (!userData.age || userData.age < 18 || userData.age > 120) {
throw new Error('Age must be between 18 and 120');
}
}
isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
// Using the service layer
async function demonstrateUserService() {
const db = new DatabaseConnection({ invalid: false });
await db.connect();
try {
const userRepository = new UserRepository(db);
const userService = new UserService(userRepository);
// Create user
const newUser = await userService.createUser({
name: 'John Doe',
email: 'john@example.com',
age: 30
});
console.log('Created user:', newUser);
// Get user
const user = await userService.getUser(newUser.id);
console.log('Fetched user:', user);
} catch (error) {
console.error('Service error:', error.message);
} finally {
await db.close();
}
}
Best Practices
Following best practices ensures robust, maintainable, and user-friendly error handling in JavaScript applications.
Error Handling Principles
// Principle 1: Fail fast and fail loudly
function processCriticalData(data) {
if (!data) {
throw new Error('Data is required for processing');
}
if (!Array.isArray(data)) {
throw new TypeError('Data must be an array');
}
if (data.length === 0) {
throw new Error('Data array cannot be empty');
}
// Continue processing
return data.map(item => item * 2);
}
// Principle 2: Handle errors at the appropriate level
function lowLevelOperation() {
// This function should not handle errors, just throw them
if (Math.random() < 0.3) {
throw new Error('Low level operation failed');
}
return 'success';
}
function midLevelOperation() {
try {
return lowLevelOperation();
} catch (error) {
// Handle or transform the error
console.error('Mid level error:', error.message);
throw new Error('Mid level operation failed');
}
}
function highLevelOperation() {
try {
return midLevelOperation();
} catch (error) {
// Handle user-facing errors
console.error('High level error:', error.message);
return { success: false, error: error.message };
}
}
// Principle 3: Provide meaningful error messages
class UserError extends Error {
constructor(message, userMessage) {
super(message);
this.name = 'UserError';
this.userMessage = userMessage || message;
}
}
function validateUserInput(input) {
if (!input) {
throw new UserError(
'Input is null or undefined',
'Please provide a value'
);
}
if (typeof input !== 'string') {
throw new UserError(
`Expected string, got ${typeof input}`,
'Please provide a valid text value'
);
}
if (input.trim().length === 0) {
throw new UserError(
'Input is empty after trimming',
'Please provide a non-empty value'
);
}
return input.trim();
}
// Principle 4: Log errors appropriately
class ErrorLogger {
constructor() {
this.errors = [];
}
log(error, context = {}) {
const logEntry = {
timestamp: new Date().toISOString(),
message: error.message,
name: error.name,
stack: error.stack,
context
};
this.errors.push(logEntry);
// In production, send to logging service
if (process.env.NODE_ENV === 'production') {
this.sendToLoggingService(logEntry);
} else {
console.error('Logged error:', logEntry);
}
}
sendToLoggingService(logEntry) {
// Implementation for external logging service
console.log('Sending to logging service:', logEntry);
}
getErrors() {
return this.errors;
}
}
const errorLogger = new ErrorLogger();
// Principle 5: Don't ignore errors
function badExample() {
try {
riskyOperation();
} catch (error) {
// Error is silently ignored
}
}
function goodExample() {
try {
riskyOperation();
} catch (error) {
errorLogger.log(error, { operation: 'riskyOperation' });
// Handle the error appropriately
throw error;
}
}
// Principle 6: Use specific error types
function processPayment(amount) {
if (amount <= 0) {
throw new ValidationError('Amount must be positive');
}
if (amount > 10000) {
throw new BusinessRuleError('Amount exceeds maximum limit');
}
// Process payment
return { success: true, amount };
}
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
class BusinessRuleError extends Error {
constructor(message) {
super(message);
this.name = 'BusinessRuleError';
}
}
// Principle 7: Provide fallback mechanisms
function safeParseJSON(jsonString, fallback = {}) {
try {
return JSON.parse(jsonString);
} catch (error) {
errorLogger.log(error, { jsonString });
return fallback;
}
}
function safeGetLocalStorage(key, fallback = null) {
try {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : fallback;
} catch (error) {
errorLogger.log(error, { key });
return fallback;
}
}
Code Organization
// Organize error handling in modules
// errors.js
export class AppError extends Error {
constructor(message, code, details = {}) {
super(message);
this.name = 'AppError';
this.code = code;
this.details = details;
this.timestamp = new Date();
}
}
export class ValidationError extends AppError {
constructor(message, field, value) {
super(message, 'VALIDATION_ERROR', { field, value });
this.field = field;
this.value = value;
}
}
export class NetworkError extends AppError {
constructor(message, statusCode, url) {
super(message, 'NETWORK_ERROR', { statusCode, url });
this.statusCode = statusCode;
this.url = url;
}
}
export class DatabaseError extends AppError {
constructor(message, query, params) {
super(message, 'DATABASE_ERROR', { query, params });
this.query = query;
this.params = params;
}
}
// errorHandler.js
import { AppError } from './errors.js';
export class ErrorHandler {
constructor(logger) {
this.logger = logger;
}
handle(error, context = {}) {
this.logError(error, context);
return this.createErrorResponse(error);
}
logError(error, context) {
const logData = {
message: error.message,
name: error.name,
code: error.code,
stack: error.stack,
timestamp: new Date().toISOString(),
context
};
this.logger.error('Application error', logData);
}
createErrorResponse(error) {
if (error instanceof ValidationError) {
return {
success: false,
error: 'Validation error',
message: error.message,
field: error.field
};
}
if (error instanceof NetworkError) {
return {
success: false,
error: 'Network error',
message: 'Unable to connect to the server'
};
}
if (error instanceof DatabaseError) {
return {
success: false,
error: 'Database error',
message: 'Data processing failed'
};
}
// Generic error
return {
success: false,
error: 'Application error',
message: 'An unexpected error occurred'
};
}
}
// service.js
import { ErrorHandler } from './errorHandler.js';
class UserService {
constructor(database, logger) {
this.database = database;
this.errorHandler = new ErrorHandler(logger);
}
async createUser(userData) {
try {
this.validateUserData(userData);
const user = await this.database.createUser(userData);
return { success: true, data: user };
} catch (error) {
return this.errorHandler.handle(error, { operation: 'createUser', userData });
}
}
validateUserData(userData) {
if (!userData.name || userData.name.trim().length < 2) {
throw new ValidationError('Name must be at least 2 characters', 'name', userData.name);
}
if (!userData.email || !this.isValidEmail(userData.email)) {
throw new ValidationError('Valid email is required', 'email', userData.email);
}
}
isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
// Error boundary pattern (React-like)
class ErrorBoundary {
constructor() {
this.errors = [];
this.onError = null;
}
catch(fn) {
this.onError = fn;
return this;
}
execute(operation) {
try {
return operation();
} catch (error) {
this.errors.push(error);
if (this.onError) {
this.onError(error);
}
return null;
}
}
executeAsync(operation) {
return operation()
.then(result => result)
.catch(error => {
this.errors.push(error);
if (this.onError) {
this.onError(error);
}
return null;
});
}
getErrors() {
return this.errors;
}
clearErrors() {
this.errors = [];
}
}
// Using error boundary
const errorBoundary = new ErrorBoundary()
.catch((error) => console.error('Caught error:', error.message));
const result1 = errorBoundary.execute(() => {
return 2 + 2;
});
const result2 = errorBoundary.execute(() => {
throw new Error('Something went wrong');
});
console.log(result1); // 4
console.log(result2); // null
Testing Error Handling
// Testing error scenarios
class ErrorTester {
constructor() {
this.testResults = [];
}
test(description, testFn) {
try {
const result = testFn();
this.testResults.push({
description,
status: 'passed',
result
});
console.log(`✓ ${description}`);
} catch (error) {
this.testResults.push({
description,
status: 'failed',
error: error.message
});
console.log(`✗ ${description}: ${error.message}`);
}
}
testAsync(description, testFn) {
return testFn()
.then(result => {
this.testResults.push({
description,
status: 'passed',
result
});
console.log(`✓ ${description}`);
})
.catch(error => {
this.testResults.push({
description,
status: 'failed',
error: error.message
});
console.log(`✗ ${description}: ${error.message}`);
});
}
expectError(description, testFn, expectedErrorType) {
try {
testFn();
this.testResults.push({
description,
status: 'failed',
error: 'Expected error but none was thrown'
});
console.log(`✗ ${description}: Expected error but none was thrown`);
} catch (error) {
const errorMatches = !expectedErrorType || error instanceof expectedErrorType;
if (errorMatches) {
this.testResults.push({
description,
status: 'passed',
result: error.message
});
console.log(`✓ ${description}: ${error.message}`);
} else {
this.testResults.push({
description,
status: 'failed',
error: `Expected ${expectedErrorType.name}, got ${error.constructor.name}`
});
console.log(`✗ ${description}: Expected ${expectedErrorType.name}, got ${error.constructor.name}`);
}
}
}
getResults() {
return this.testResults;
}
getSummary() {
const passed = this.testResults.filter(r => r.status === 'passed').length;
const failed = this.testResults.filter(r => r.status === 'failed').length;
const total = this.testResults.length;
return {
total,
passed,
failed,
passRate: ((passed / total) * 100).toFixed(2) + '%'
};
}
}
// Testing error handling functions
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
function parseJSON(jsonString) {
const result = JSON.parse(jsonString);
return result;
}
function validateEmail(email) {
if (!email) {
throw new Error('Email is required');
}
if (!email.includes('@')) {
throw new Error('Invalid email format');
}
return email;
}
// Running tests
const tester = new ErrorTester();
// Test successful cases
tester.test('Divide 10 by 2', () => divide(10, 2));
tester.test('Parse valid JSON', () => parseJSON('{"name": "John"}'));
tester.test('Validate valid email', () => validateEmail('test@example.com'));
// Test error cases
tester.expectError('Divide by zero', () => divide(10, 0), Error);
tester.expectError('Parse invalid JSON', () => parseJSON('invalid json'), SyntaxError);
tester.expectError('Validate empty email', () => validateEmail(''), Error);
tester.expectError('Validate invalid email', () => validateEmail('invalid'), Error);
// Test async error handling
async function fetchData(url) {
if (url === 'error') {
throw new Error('Network error');
}
return { data: 'success' };
}
tester.testAsync('Fetch valid data', () => fetchData('valid'));
tester.testAsync('Fetch with error', () => fetchData('error'));
// Test custom error types
class CustomError extends Error {
constructor(message) {
super(message);
this.name = 'CustomError';
}
}
function throwCustomError() {
throw new CustomError('Custom error message');
}
tester.expectError('Throw custom error', throwCustomError, CustomError);
// Display results
console.log('\nTest Summary:');
console.log(tester.getSummary());
console.log('\nDetailed Results:');
tester.getResults().forEach(result => {
console.log(`${result.status.toUpperCase()}: ${result.description}`);
if (result.error) {
console.log(` Error: ${result.error}`);
}
});
// Performance testing for error handling
function performanceTest() {
const iterations = 100000;
console.time('Try-catch performance');
for (let i = 0; i < iterations; i++) {
try {
const result = i / 2;
} catch (error) {
// Handle error
}
}
console.timeEnd('Try-catch performance');
console.time('No try-catch performance');
for (let i = 0; i < iterations; i++) {
const result = i / 2;
}
console.timeEnd('No try-catch performance');
}
performanceTest();
Summary
Key Takeaways
- Try-catch is JavaScript's fundamental error handling mechanism
- Finally blocks always execute, regardless of errors
- JavaScript has several built-in error types (Error, TypeError, ReferenceError, etc.)
- Custom error classes provide better error categorization
- Async error handling requires special consideration with promises and async/await
- Error handling should be implemented at appropriate abstraction levels
- Meaningful error messages improve debugging and user experience
- Error logging and monitoring are essential for production applications
Best Practices
- Fail fast and fail loud with clear error messages
- Handle errors at the appropriate level of abstraction
- Create custom error classes for better error categorization
- Log errors with sufficient context for debugging
- Provide fallback mechanisms for non-critical operations
- Use try-catch-finally for resource cleanup
- Implement retry mechanisms for transient failures
- Test error handling scenarios thoroughly
- Don't ignore errors - always handle or re-throw them
- Use specific error types for different failure scenarios
- Implement error boundaries for better error isolation
- Provide user-friendly error messages while logging technical details
Common Pitfalls
- Silently catching errors without proper handling
- Using generic Error type instead of specific error types
- Not providing enough context in error messages
- Forgetting to include finally blocks for cleanup
- Not handling errors in async operations properly
- Catching errors at the wrong level of abstraction
- Not logging errors for debugging purposes
- Exposing internal error details to end users
- Not implementing retry mechanisms for transient failures
- Ignoring error handling in production code
- Using try-catch for flow control instead of error conditions
- Not testing error handling scenarios
- Catching too broadly and masking important errors