JavaScript Error Handling & Debugging
Master error handling techniques, try-catch blocks, and debugging strategies for robust JavaScript applications. This is a foundational concept in programming and web interactivity that professional developers rely on daily. The explanations below are written to be beginner-friendly while covering the depth and nuance that comes from real-world JavaScript experience. Take your time with each section and practice the examples
Error Handling Fundamentals
Error handling is crucial for building robust applications. JavaScript provides try-catch blocks and error objects to handle exceptions gracefully and prevent application crashes.. This is an essential concept that every JavaScript developer must understand thoroughly. In professional development environments, getting this right can mean the difference between code that works reliably and code that breaks in production. The following sections break this down into clear, digestible pieces with practical examples you can try immediately
Try-Catch Blocks
// Basic try-catch
try {
const result = 10 / 0;
console.log(result);
} catch (error) {
console.error('An error occurred:', error.message);
}
// Try-catch with finally
try {
const data = JSON.parse('invalid json');
console.log(data);
} catch (error) {
console.error('JSON parsing error:', error.message);
} finally {
console.log('This always executes');
}
// Multiple catch blocks (not supported in JavaScript)
// Use if-else instead
try {
const result = someRiskyOperation();
console.log(result);
} catch (error) {
if (error instanceof TypeError) {
console.error('Type error:', error.message);
} else if (error instanceof ReferenceError) {
console.error('Reference error:', error.message);
} else {
console.error('Unknown error:', error.message);
}
}
// Async error handling
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
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);
return null;
}
}
// Custom error classes
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;
}
}
// Using custom errors
function validateUser(user) {
if (!user.name) {
throw new ValidationError('Name is required', 'name');
}
if (!user.email) {
throw new ValidationError('Email is required', 'email');
}
if (user.age < 0) {
throw new ValidationError('Age must be positive', 'age');
}
}
try {
validateUser({ name: '', email: 'test@example.com', age: 25 });
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Validation error in ${error.field}: ${error.message}`);
} else {
console.error('Unknown error:', error.message);
}
}Error Handling Patterns
// Error handling patterns
// 1. Error boundary pattern
function withErrorBoundary(operation, fallback) {
try {
return operation();
} catch (error) {
console.error('Operation failed:', error);
return fallback;
}
}
const result = withErrorBoundary(
() => JSON.parse('invalid json'),
{ error: 'Failed to parse data' }
);
// 2. Retry pattern
async function retryOperation(operation, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
console.log(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
// 3. Error logging
class ErrorLogger {
constructor() {
this.errors = [];
}
log(error, context = {}) {
const errorEntry = {
timestamp: new Date(),
error: error.message,
stack: error.stack,
context
};
this.errors.push(errorEntry);
console.error('Error logged:', errorEntry);
// In production, send to error tracking service
this.sendToErrorService(errorEntry);
}
sendToErrorService(errorEntry) {
// Simulate sending to error tracking service
console.log('Sending to error service:', errorEntry);
}
getErrors() {
return this.errors;
}
}
const logger = new ErrorLogger();
// 4. Graceful degradation
function loadFeature(featureName) {
try {
const feature = require(`./features/${featureName}`);
return feature;
} catch (error) {
console.warn(`Feature ${featureName} not available, using fallback`);
return getFallbackFeature(featureName);
}
}
function getFallbackFeature(featureName) {
const fallbacks = {
'advanced-chart': () => ({ render: () => console.log('Basic chart rendered') }),
'real-time-updates': () => ({ start: () => console.log('Polling updates started') })
};
return fallbacks[featureName] || (() => ({ render: () => console.log('Default feature') }));
}
// 5. Input validation
function validateInput(input, schema) {
const errors = [];
for (const [field, rules] of Object.entries(schema)) {
const value = input[field];
if (rules.required && !value) {
errors.push(`${field} is required`);
}
if (value && rules.type && typeof value !== rules.type) {
errors.push(`${field} must be a ${rules.type}`);
}
if (value && rules.min && value < rules.min) {
errors.push(`${field} must be at least ${rules.min}`);
}
if (value && rules.max && value > rules.max) {
errors.push(`${field} must be at most ${rules.max}`);
}
}
if (errors.length > 0) {
throw new ValidationError(errors.join(', '));
}
return true;
}
// Usage
const userSchema = {
name: { required: true, type: 'string' },
age: { required: true, type: 'number', min: 0, max: 120 },
email: { required: true, type: 'string' }
};
try {
validateInput({ name: 'John', age: 25, email: 'john@example.com' }, userSchema);
console.log('Input is valid');
} catch (error) {
console.error('Validation failed:', error.message);
}Practice Exercise: Error Handling
// Exercise: Build a Robust Calculator
class SafeCalculator {
constructor() {
this.history = [];
}
add(a, b) {
try {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Both arguments must be numbers');
}
const result = a + b;
this.history.push({ operation: 'add', a, b, result });
return result;
} catch (error) {
this.logError(error, { operation: 'add', a, b });
throw error;
}
}
divide(a, b) {
try {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Both arguments must be numbers');
}
if (b === 0) {
throw new Error('Division by zero');
}
const result = a / b;
this.history.push({ operation: 'divide', a, b, result });
return result;
} catch (error) {
this.logError(error, { operation: 'divide', a, b });
throw error;
}
}
sqrt(number) {
try {
if (typeof number !== 'number') {
throw new TypeError('Argument must be a number');
}
if (number < 0) {
throw new Error('Cannot calculate square root of negative number');
}
const result = Math.sqrt(number);
this.history.push({ operation: 'sqrt', number, result });
return result;
} catch (error) {
this.logError(error, { operation: 'sqrt', number });
throw error;
}
}
logError(error, context) {
console.error('Calculator error:', {
message: error.message,
type: error.constructor.name,
context,
timestamp: new Date()
});
}
getHistory() {
return this.history;
}
clearHistory() {
this.history = [];
}
}
// Exercise: Build an API Client with Error Handling
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.retryAttempts = 3;
this.retryDelay = 1000;
}
async request(endpoint, options = {}) {
let lastError;
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
try {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
if (!response.ok) {
throw new NetworkError(
`HTTP ${response.status}: ${response.statusText}`,
response.status
);
}
return await response.json();
} catch (error) {
lastError = error;
if (attempt < this.retryAttempts) {
console.log(`Attempt ${attempt} failed, retrying in ${this.retryDelay}ms...`);
await this.delay(this.retryDelay * attempt);
}
}
}
throw lastError;
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async getUsers() {
return this.request('/users');
}
async createUser(userData) {
return this.request('/users', {
method: 'POST',
body: JSON.stringify(userData)
});
}
async updateUser(id, userData) {
return this.request(`/users/${id}`, {
method: 'PUT',
body: JSON.stringify(userData)
});
}
async deleteUser(id) {
return this.request(`/users/${id}`, {
method: 'DELETE'
});
}
}
// Test the error handling
const calculator = new SafeCalculator();
try {
console.log(calculator.add(5, 3)); // 8
console.log(calculator.divide(10, 2)); // 5
console.log(calculator.sqrt(16)); // 4
console.log(calculator.divide(10, 0)); // Error: Division by zero
} catch (error) {
console.error('Calculator error:', error.message);
}
console.log('Calculator history:', calculator.getHistory());