Skip to main content

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
  1. Validate inputs early (fail fast)
  2. Use specific error types
  3. Provide descriptive messages
  4. Log errors appropriately
  5. Show user-friendly messages
  6. 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

  1. Create a custom error hierarchy for an e-commerce application
  2. Implement a robust API client with comprehensive error handling
  3. Build an error logging utility that groups similar errors
  4. Create a validation library that throws descriptive errors
  5. Implement retry logic for failed API requests
  6. Build a debugging helper that logs function calls and returns
  7. Create an error boundary component for React
  8. Implement a global error handler for a web application

Additional Resources