Why Error Handling Matters
Imagine you're driving a car, and suddenly the engine starts making strange noises. Would you prefer to ignore it and keep driving, or would you rather have a dashboard warning light that tells you exactly what's wrong? Error handling in JavaScript is like that dashboard - it helps us identify, understand, and gracefully handle problems in our code before they crash our entire application.
Understanding Errors in JavaScript
JavaScript has several built-in error types, each serving a specific purpose:
- Error: Base error object
- SyntaxError: Invalid JavaScript syntax
- ReferenceError: Invalid reference to a variable
- TypeError: Value is not of expected type
- RangeError: Numeric value out of range
- URIError: Invalid URI function usage
- EvalError: Error in eval() function
The Try/Catch Statement
The try/catch statement allows you to "try" a block of code and "catch" any errors that occur:
// Basic try/catch syntax
try {
// Code that might throw an error
riskyOperation();
} catch (error) {
// Handle the error
console.error('An error occurred:', error.message);
}
How It Works
Basic Error Handling Examples
Example 1: Handling Reference Errors
function greetUser(name) {
try {
// Attempting to use an undefined variable
console.log(message); // 'message' is not defined
return `Hello, ${name}!`;
} catch (error) {
console.error('Caught an error:', error.name);
console.error('Error message:', error.message);
// Provide a fallback behavior
return `Hello, ${name}! (Error occurred)`;
}
}
console.log(greetUser('Alice'));
// Output:
// Caught an error: ReferenceError
// Error message: message is not defined
// Hello, Alice! (Error occurred)
Example 2: Handling Type Errors
function calculateTotal(items) {
try {
// This will fail if items is not an array
return items.reduce((sum, item) => sum + item.price, 0);
} catch (error) {
console.error('Error calculating total:', error.message);
// Return a safe default value
return 0;
}
}
// Works fine with valid data
console.log(calculateTotal([{price: 10}, {price: 20}])); // 30
// Handles error gracefully with invalid data
console.log(calculateTotal(null)); // 0
console.log(calculateTotal("not an array")); // 0
The Finally Block
The finally block runs regardless of whether an error occurred:
function processFile(filename) {
let file;
try {
file = openFile(filename);
const data = readFile(file);
processData(data);
return 'Success';
} catch (error) {
console.error('Error processing file:', error.message);
return 'Failed';
} finally {
// This always runs, whether there was an error or not
if (file) {
closeFile(file);
console.log('File closed');
}
}
}
// Simulated file operations
function openFile(name) {
console.log(`Opening ${name}`);
return { name, isOpen: true };
}
function readFile(file) {
if (Math.random() > 0.5) {
throw new Error('Read error');
}
return 'file contents';
}
function processData(data) {
console.log('Processing:', data);
}
function closeFile(file) {
file.isOpen = false;
}
// Test the function
console.log(processFile('data.txt'));
console.log(processFile('data.txt'));
Throwing Custom Errors
You can create and throw your own errors to handle specific situations:
// Creating custom error classes
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.name = 'AuthenticationError';
}
}
// Using custom errors
function validateUser(user) {
if (!user) {
throw new ValidationError('User object is required');
}
if (!user.email) {
throw new ValidationError('Email is required');
}
if (!user.email.includes('@')) {
throw new ValidationError('Invalid email format');
}
return true;
}
function authenticateUser(credentials) {
if (!credentials.token) {
throw new AuthenticationError('Authentication token missing');
}
if (credentials.token !== 'valid-token') {
throw new AuthenticationError('Invalid authentication token');
}
return true;
}
// Using the functions with proper error handling
try {
validateUser({ email: 'invalid-email' });
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation failed:', error.message);
} else {
console.log('Unexpected error:', error);
}
}
try {
authenticateUser({ token: 'invalid-token' });
} catch (error) {
if (error instanceof AuthenticationError) {
console.log('Authentication failed:', error.message);
} else {
console.log('Unexpected error:', error);
}
}
Error Handling in Asynchronous Code
Try/Catch with Async/Await
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const userData = await response.json();
if (!userData.email) {
throw new ValidationError('User data missing email');
}
return userData;
} catch (error) {
// Different handling based on error type
if (error instanceof ValidationError) {
console.error('Data validation error:', error.message);
return null;
} else if (error.name === 'TypeError') {
console.error('Network error:', error.message);
return null;
} else {
console.error('Unexpected error:', error);
throw error; // Re-throw unexpected errors
}
}
}
// Usage
async function displayUserProfile(userId) {
const userData = await fetchUserData(userId);
if (userData) {
console.log('User profile:', userData);
} else {
console.log('Failed to load user profile');
}
}
Error Handling with Promises
function fetchDataWithPromises(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (!data.isValid) {
throw new ValidationError('Invalid data received');
}
return data;
})
.catch(error => {
// Handle different types of errors
if (error instanceof ValidationError) {
console.error('Validation error:', error.message);
return { error: 'Invalid data', details: error.message };
} else if (error.name === 'TypeError') {
console.error('Network error:', error.message);
return { error: 'Network issue', details: error.message };
} else {
console.error('Unexpected error:', error);
return { error: 'Unknown error', details: error.message };
}
});
}
Best Practices for Error Handling
1. Be Specific with Error Types
// Bad practice - generic error handling
try {
doSomething();
} catch (e) {
console.log('Error occurred');
}
// Good practice - specific error handling
try {
doSomething();
} catch (error) {
if (error instanceof TypeError) {
console.error('Type error:', error.message);
// Handle type error specifically
} else if (error instanceof RangeError) {
console.error('Range error:', error.message);
// Handle range error specifically
} else {
console.error('Unexpected error:', error);
// Handle or re-throw unexpected errors
}
}
2. Don't Swallow Errors
// Bad practice - silently swallowing errors
try {
riskyOperation();
} catch (error) {
// Error is caught but not handled or logged
}
// Good practice - always handle or log errors
try {
riskyOperation();
} catch (error) {
console.error('Operation failed:', error);
// Either handle the error or re-throw it
throw error;
}
3. Provide Meaningful Error Messages
// Bad practice - vague error message
function divide(a, b) {
if (b === 0) {
throw new Error('Error');
}
return a / b;
}
// Good practice - descriptive error message
function divide(a, b) {
if (b === 0) {
throw new Error(`Cannot divide ${a} by zero`);
}
return a / b;
}
Real-World Error Handling Patterns
Pattern 1: Error Boundary for UI Components
class ErrorBoundary {
constructor() {
this.hasError = false;
this.error = null;
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Component error:', error);
console.error('Error info:', errorInfo);
// Log error to error reporting service
logErrorToService(error, errorInfo);
}
render() {
if (this.hasError) {
return `
Something went wrong
${this.error.message}
`;
}
return this.children;
}
}
Pattern 2: Global Error Handler
// Global error handler for uncaught errors
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global error caught:', {
message,
source,
line: lineno,
column: colno,
error
});
// Log to error tracking service
logErrorToService({
type: 'uncaught',
message,
source,
line: lineno,
column: colno,
stack: error?.stack
});
// Prevent default browser error handling
return true;
};
// Global handler for unhandled promise rejections
window.onunhandledrejection = function(event) {
console.error('Unhandled promise rejection:', event.reason);
// Log to error tracking service
logErrorToService({
type: 'unhandledRejection',
reason: event.reason,
promise: event.promise
});
// Prevent default browser handling
event.preventDefault();
};
Error Logging and Monitoring
// Simple error logging service
class ErrorLogger {
static log(error, context = {}) {
const errorInfo = {
timestamp: new Date().toISOString(),
message: error.message,
stack: error.stack,
type: error.name,
context,
url: window.location.href,
userAgent: navigator.userAgent
};
// In development, log to console
if (process.env.NODE_ENV === 'development') {
console.error('Error logged:', errorInfo);
}
// In production, send to error tracking service
if (process.env.NODE_ENV === 'production') {
fetch('/api/log-error', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(errorInfo)
}).catch(err => {
console.error('Failed to log error:', err);
});
}
}
}
// Usage example
try {
performRiskyOperation();
} catch (error) {
ErrorLogger.log(error, {
operation: 'performRiskyOperation',
userId: currentUser.id,
additionalInfo: 'Failed during data processing'
});
// Handle the error for the user
showErrorMessage('Operation failed. Please try again.');
}
Practice Exercises
Exercise 1: Safe JSON Parser
Create a function that safely parses JSON and handles errors:
function safeJSONParse(jsonString, defaultValue = null) {
// Your code here
// Should parse JSON string and return parsed object
// If parsing fails, return defaultValue
// Log appropriate error messages
}
// Test cases
console.log(safeJSONParse('{"name": "John"}')); // Should return object
console.log(safeJSONParse('invalid json')); // Should return null
console.log(safeJSONParse('invalid json', {})); // Should return {}
Exercise 2: Retry with Error Handling
Create a function that retries an operation with error handling:
async function retryOperation(operation, maxAttempts = 3) {
// Your code here
// Should attempt operation up to maxAttempts times
// Should handle and log errors
// Should wait between retries
// Should throw error if all attempts fail
}
// Test with a flaky operation
async function flakyOperation() {
if (Math.random() > 0.7) {
return 'Success!';
}
throw new Error('Random failure');
}
Key Takeaways
- Try/catch blocks help manage errors gracefully
- Finally blocks ensure cleanup code always runs
- Custom errors provide better error categorization
- Different error types require different handling strategies
- Always log errors for debugging and monitoring
- Never silently swallow errors
- Provide meaningful error messages for better debugging