Module 12: Error Handling and Debugging
Writing code is easy; writing robust, error-free code is hard. This module teaches you how to handle errors gracefully and debug effectively.
1. Understanding Errors
1.1 Types of Errors
1. Syntax Errors
// Missing closing bracket
const obj = { name: 'John' // ❌ SyntaxError
// Invalid syntax
const function = 'test'; // ❌ SyntaxError: Unexpected token
2. Runtime Errors
// ReferenceError: Variable doesn't exist
console.log(undefinedVariable); // ❌ ReferenceError
// TypeError: Invalid operation
null.toString(); // ❌ TypeError
// RangeError: Invalid value
new Array(-1); // ❌ RangeError
3. Logical Errors
// Wrong logic (no error thrown, but wrong result)
function calculateTotal(price, quantity) {
return price + quantity; // Should be price * quantity ❌
}
1.2 Error Object
const error = new Error('Something went wrong');
console.log(error.name); // "Error"
console.log(error.message); // "Something went wrong"
console.log(error.stack); // Stack trace
2. Built-in Error Types
2.1 Error Types
// Error (generic)
throw new Error('Generic error');
// ReferenceError (variable not found)
console.log(nonExistentVar); // ReferenceError
// TypeError (wrong type)
const num = 42;
num.toUpperCase(); // TypeError
// RangeError (value out of range)
function recursion() { recursion(); }
recursion(); // RangeError: Maximum call stack size exceeded
// SyntaxError (invalid syntax)
eval('const x ='); // SyntaxError
// URIError (invalid URI handling)
decodeURIComponent('%'); // URIError
2.2 Creating Custom Errors
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;
}
}
class NotFoundError extends Error {
constructor(resource) {
super(`${resource} not found`);
this.name = 'NotFoundError';
}
}
// Usage
throw new ValidationError('Email is invalid');
throw new NetworkError('Request failed', 500);
throw new NotFoundError('User');
3. try...catch Statement
3.1 Basic Usage
try {
// Code that might throw an error
const data = JSON.parse(invalidJSON);
console.log(data);
} catch (error) {
// Handle the error
console.error('Error occurred:', error.message);
}
console.log('Program continues...');
3.2 Catching Specific Errors
try {
const user = getUser(userId);
processUser(user);
} catch (error) {
if (error instanceof ValidationError) {
console.error('Validation failed:', error.message);
} else if (error instanceof NotFoundError) {
console.error('User not found');
} else if (error instanceof NetworkError) {
console.error('Network error:', error.statusCode);
} else {
console.error('Unexpected error:', error);
}
}
3.3 finally Block
let file;
try {
file = openFile('data.txt');
processFile(file);
} catch (error) {
console.error('Error processing file:', error);
} finally {
// Always executes, even if error occurred
if (file) {
closeFile(file);
}
console.log('Cleanup completed');
}
3.4 Nested try...catch
try {
const data = fetchData();
try {
const parsed = JSON.parse(data);
console.log(parsed);
} catch (parseError) {
console.error('Parse error:', parseError.message);
}
} catch (fetchError) {
console.error('Fetch error:', fetchError.message);
}
Use finally for Cleanup
The finally block is perfect for cleanup operations like closing files, clearing timers, or releasing resources.
4. throw Statement
4.1 Throwing Errors
function divide(a, b) {
if (b === 0) {
throw new Error('Cannot divide by zero');
}
return a / b;
}
try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error(error.message); // "Cannot divide by zero"
}
4.2 Throwing Custom Values
function validateAge(age) {
if (age < 0) {
throw 'Age cannot be negative'; // String
}
if (age < 18) {
throw { code: 'UNDERAGE', message: 'Must be 18+' }; // Object
}
return true;
}
try {
validateAge(-5);
} catch (error) {
console.error(error); // "Age cannot be negative"
}
Always Throw Error Objects
While you can throw any value, always prefer throwing Error objects for better stack traces.
4.3 Re-throwing Errors
function processData(data) {
try {
if (!data) {
throw new Error('No data provided');
}
return JSON.parse(data);
} catch (error) {
console.error('Processing failed:', error.message);
throw error; // Re-throw to caller
}
}
try {
const result = processData(null);
} catch (error) {
console.error('Caught in caller:', error.message);
}
5. Error Handling Patterns
5.1 Safe Function Wrapper
function tryCatch(fn, fallbackValue = null) {
try {
return fn();
} catch (error) {
console.error('Error:', error.message);
return fallbackValue;
}
}
// Usage
const data = tryCatch(() => JSON.parse(userInput), {});
const value = tryCatch(() => localStorage.getItem('key'), 'default');
5.2 Error-First Callbacks (Node.js Style)
function readFile(path, callback) {
try {
const data = fs.readFileSync(path, 'utf8');
callback(null, data); // null = no error
} catch (error) {
callback(error, null); // error first
}
}
// Usage
readFile('data.txt', (error, data) => {
if (error) {
console.error('Error reading file:', error);
return;
}
console.log('File content:', data);
});
5.3 Result/Error Pattern
function safeParse(json) {
try {
return {
success: true,
data: JSON.parse(json),
error: null
};
} catch (error) {
return {
success: false,
data: null,
error: error.message
};
}
}
// Usage
const result = safeParse(userInput);
if (result.success) {
console.log('Parsed:', result.data);
} else {
console.error('Error:', result.error);
}
5.4 Guard Clauses
function processUser(user) {
// Guard clauses at the top
if (!user) {
throw new Error('User is required');
}
if (!user.email) {
throw new ValidationError('Email is required');
}
if (!user.email.includes('@')) {
throw new ValidationError('Invalid email format');
}
// Main logic
return saveUser(user);
}
Fail Fast
Use guard clauses to validate inputs early and throw errors immediately rather than nesting logic.
6. Async Error Handling
6.1 Promise catch()
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
console.error('Fetch failed:', error);
});
6.2 async/await with try...catch
async function fetchUser(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('Error fetching user:', error.message);
throw error; // Re-throw or handle
}
}
// Usage
async function main() {
try {
const user = await fetchUser(123);
console.log('User:', user);
} catch (error) {
console.error('Failed to load user');
}
}
6.3 Promise.allSettled()
async function fetchMultipleUsers(ids) {
const promises = ids.map(id => fetchUser(id));
const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`User ${ids[index]}:`, result.value);
} else {
console.error(`Failed to fetch user ${ids[index]}:`, result.reason);
}
});
}
fetchMultipleUsers([1, 2, 999, 4]);
6.4 Unhandled Promise Rejection
// Global handler for unhandled rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled rejection:', event.reason);
event.preventDefault(); // Prevent default browser behavior
});
// Promise without catch
Promise.reject('Something went wrong'); // Will trigger unhandledrejection
7. Debugging Techniques
7.1 console Methods
// Basic logging
console.log('Simple message');
console.info('Info message');
console.warn('Warning message');
console.error('Error message');
// Formatted output
const user = { name: 'John', age: 30 };
console.log('User:', user);
console.table(user); // Table format
// Grouping
console.group('User Details');
console.log('Name:', user.name);
console.log('Age:', user.age);
console.groupEnd();
// Timing
console.time('operation');
// ... some code
console.timeEnd('operation'); // Logs time elapsed
// Stack trace
console.trace('Trace point');
// Counting
for (let i = 0; i < 5; i++) {
console.count('Loop'); // Loop: 1, Loop: 2, etc.
}
// Assertions
console.assert(1 === 2, '1 should equal 2'); // Logs error if false
7.2 Debugger Statement
function calculateTotal(items) {
let total = 0;
debugger; // Execution pauses here in DevTools
for (const item of items) {
total += item.price * item.quantity;
}
return total;
}
7.3 Breakpoints in DevTools
// Use Chrome DevTools:
// - Sources tab → Click line number to set breakpoint
// - Conditional breakpoints: Right-click → Add conditional breakpoint
// - Watch expressions to monitor variables
// - Call stack to see execution flow
7.4 Error Boundaries (React)
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
8. Production Error Handling
8.1 Global Error Handler
// Catch all errors
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
// Send to error tracking service
logErrorToService({
message: event.error.message,
stack: event.error.stack,
url: window.location.href,
userAgent: navigator.userAgent
});
// Prevent default browser error handling
event.preventDefault();
});
8.2 Error Logging Service
class ErrorLogger {
static log(error, context = {}) {
const errorData = {
message: error.message,
stack: error.stack,
name: error.name,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent,
context
};
// Send to backend
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
}).catch(console.error);
// Also log locally
console.error('Error logged:', errorData);
}
}
// Usage
try {
riskyOperation();
} catch (error) {
ErrorLogger.log(error, { userId: currentUser.id });
}
8.3 Error Monitoring Services
// Sentry integration
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "YOUR_SENTRY_DSN",
environment: process.env.NODE_ENV,
beforeSend(event) {
// Modify or filter errors before sending
return event;
}
});
// Usage
try {
performAction();
} catch (error) {
Sentry.captureException(error);
}
// With context
Sentry.setUser({ id: '123', email: 'user@example.com' });
Sentry.setTag('page', 'checkout');
Sentry.captureMessage('Checkout completed');
9. Defensive Programming
9.1 Input Validation
function processPayment(amount, currency) {
// Validate inputs
if (typeof amount !== 'number' || amount <= 0) {
throw new ValidationError('Amount must be a positive number');
}
if (typeof currency !== 'string' || currency.length !== 3) {
throw new ValidationError('Currency must be a 3-letter code');
}
// Process payment
return { amount, currency, status: 'success' };
}
9.2 Null/Undefined Checks
function getUserEmail(user) {
// Optional chaining (safe)
return user?.profile?.email ?? 'no-email@example.com';
// Traditional approach
if (!user || !user.profile) {
return 'no-email@example.com';
}
return user.profile.email || 'no-email@example.com';
}
9.3 Type Checking
function concatenate(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') {
throw new TypeError('Both arguments must be strings');
}
return a + b;
}
// Runtime type checking with TypeScript-like assertions
function assertString(value, name) {
if (typeof value !== 'string') {
throw new TypeError(`${name} must be a string`);
}
}
function processName(name) {
assertString(name, 'name');
return name.toUpperCase();
}
10. Best Practices
10.1 Error Messages
// ❌ Bad: Vague error messages
throw new Error('Invalid');
throw new Error('Error');
// ✅ Good: Descriptive error messages
throw new Error('Email address is required');
throw new Error('Price must be between 0 and 10000');
throw new ValidationError('Invalid email format: missing @ symbol');
10.2 Don't Swallow Errors
// ❌ Bad: Silent failures
try {
riskyOperation();
} catch (error) {
// Nothing - error is lost!
}
// ✅ Good: Log or handle
try {
riskyOperation();
} catch (error) {
console.error('Operation failed:', error);
// Or show user-friendly message
showNotification('Operation failed. Please try again.');
}
10.3 Fail Fast
// ❌ Bad: Checking at the end
function processOrder(order) {
const items = order.items;
const total = calculateTotal(items);
if (!order || !order.items) {
return null; // Too late!
}
return total;
}
// ✅ Good: Validate early
function processOrder(order) {
if (!order || !order.items) {
throw new Error('Invalid order');
}
return calculateTotal(order.items);
}
10.4 Use Custom Errors
// ❌ Bad: Generic errors
throw new Error('User validation failed');
// ✅ Good: Specific error types
throw new ValidationError('Email is required');
throw new NotFoundError('User');
throw new AuthenticationError('Invalid credentials');
Error Handling Strategy
- Validate inputs early (fail fast)
- Use specific error types
- Provide descriptive messages
- Log errors appropriately
- Show user-friendly messages
- Don't swallow errors silently
11. Testing Error Handling
11.1 Testing with Jest
describe('divide function', () => {
it('should throw error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
});
it('should throw Error instance', () => {
expect(() => divide(10, 0)).toThrow(Error);
});
it('should return correct result', () => {
expect(divide(10, 2)).toBe(5);
});
});
describe('async function', () => {
it('should handle errors', async () => {
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
});
Summary
In this module, you learned:
- ✅ Understanding different types of errors
- ✅ Using try...catch...finally for error handling
- ✅ Creating and throwing custom errors
- ✅ Error handling patterns and best practices
- ✅ Async error handling with Promises and async/await
- ✅ Debugging techniques and tools
- ✅ Production error handling and monitoring
- ✅ Defensive programming strategies
Next Steps
In Module 13, you'll learn about DOM Manipulation to interact with HTML elements dynamically.
Practice Exercises
- Create a custom error hierarchy for an e-commerce application
- Implement a robust API client with comprehensive error handling
- Build an error logging utility that groups similar errors
- Create a validation library that throws descriptive errors
- Implement retry logic for failed API requests
- Build a debugging helper that logs function calls and returns
- Create an error boundary component for React
- Implement a global error handler for a web application