Introduction to Error Handling
Error handling is a critical aspect of writing robust JavaScript applications. Without proper error handling, unexpected issues can crash your program, lead to unintended behavior, or expose security vulnerabilities.
Think of error handling like safety nets for circus performers. The artist may be skilled, but safety nets are essential - not because failure is expected, but because it must be anticipated.
Try/Catch Block Fundamentals
The try/catch statement creates a block of code where exceptions can be "caught" and handled rather than causing the program to crash.
try {
// Code that might cause an error
const result = riskyOperation();
} catch (error) {
// Code that handles the error
console.error('Something went wrong:', error.message);
}
The basic structure consists of:
- try - Contains code that might throw an exception
- catch - Contains code that executes if an exception occurs
- finally - (Optional) Contains code that always executes, regardless of exceptions
The Try Block
The try block wraps code that might throw an exception. JavaScript will monitor this code for exceptions and redirect to the catch block if one occurs.
try {
// Risky code goes here
const user = JSON.parse(userDataString); // Might throw if invalid JSON
document.getElementById('nonexistentElement').textContent = 'Hello'; // Might throw DOM error
const result = 10 / 0; // Arithmetic operation that might cause issues
}
Real-world example: Imagine you're building an application that fetches user data from localStorage. The data might be corrupted or missing:
try {
const userData = localStorage.getItem('userData');
if (!userData) {
throw new Error('User data not found in localStorage');
}
const user = JSON.parse(userData);
displayUserProfile(user);
} catch (error) {
console.error('Failed to load user profile:', error.message);
showLoginForm(); // Fallback behavior
}
The Catch Block
The catch block receives the exception object and contains code to handle the error gracefully.
The error parameter (often named 'error', 'err', or 'e') is an object that contains information about the exception:
try {
throw new Error('Something bad happened');
} catch (error) {
console.error(error.name); // "Error"
console.error(error.message); // "Something bad happened"
console.error(error.stack); // Stack trace showing where the error occurred
}
You can customize error handling based on the error type:
try {
// Some risky code
} catch (error) {
if (error instanceof TypeError) {
// Handle type errors
} else if (error instanceof ReferenceError) {
// Handle reference errors
} else {
// Handle all other errors
}
}
The Finally Block
The finally block contains code that always executes after the try and catch blocks, regardless of whether an exception occurred.
try {
const connection = openDatabaseConnection();
// Database operations...
} catch (error) {
console.error('Database error:', error.message);
} finally {
// This code always runs, ensuring resources are released
closeDatabaseConnection();
}
The finally block is perfect for:
- Cleaning up resources (closing files, database connections)
- Resetting state variables
- Logging completion of operations regardless of outcome
Real-world example: Loading indicator in a web application
const showLoader = () => document.getElementById('loader').style.display = 'block';
const hideLoader = () => document.getElementById('loader').style.display = 'none';
async function fetchUserData(userId) {
showLoader();
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const userData = await response.json();
return userData;
} catch (error) {
console.error('Failed to fetch user data:', error);
displayErrorMessage('Could not load user information. Please try again.');
return null;
} finally {
hideLoader(); // Always hide the loader, whether successful or not
}
}
Error Objects in JavaScript
JavaScript has several built-in error object types that provide specific information about different error conditions:
- Error: Base object for all errors
- SyntaxError: Occurs when there's a syntax mistake in the code
- ReferenceError: Occurs when referencing an undeclared variable
- TypeError: Occurs when a value is not of the expected type
- RangeError: Occurs when a value is outside the allowed range
- URIError: Occurs with incorrect use of URI functions
- EvalError: Occurs with the eval() function (rare in modern JavaScript)
- AggregateError: Represents multiple errors wrapped in a single error (ES2021)
Creating Custom Error Objects
You can create custom error types by extending the built-in Error class:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
// Usage
try {
const email = user.email;
if (!email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Validation failed for ${error.field}: ${error.message}`);
highlightField(error.field);
} else {
console.error('An unexpected error occurred:', error);
}
}
Creating custom error classes helps organize your error handling and provides more context about the error's nature.
Real-World Error Handling Strategies
Form Validation
function validateForm(formData) {
try {
if (!formData.name) {
throw new ValidationError('Name is required', 'name');
}
if (!formData.email || !formData.email.includes('@')) {
throw new ValidationError('Valid email is required', 'email');
}
if (formData.password.length < 8) {
throw new ValidationError('Password must be at least 8 characters', 'password');
}
return true; // Form is valid
} catch (error) {
if (error instanceof ValidationError) {
displayFieldError(error.field, error.message);
} else {
console.error('Validation error:', error);
displayGeneralError('Form validation failed');
}
return false; // Form is invalid
}
}
API Request Handling
async function fetchData(endpoint) {
try {
const response = await fetch(endpoint);
if (!response.ok) {
if (response.status === 404) {
throw new NotFoundError(`Resource not found at ${endpoint}`);
} else if (response.status === 401 || response.status === 403) {
throw new AuthorizationError('You do not have permission to access this resource');
} else {
throw new Error(`HTTP error! Status: ${response.status}`);
}
}
const data = await response.json();
return data;
} catch (error) {
if (error instanceof NotFoundError) {
showNotFoundMessage();
} else if (error instanceof AuthorizationError) {
redirectToLogin();
} else if (error.name === 'SyntaxError') {
console.error('Invalid JSON response from server');
showServerErrorMessage();
} else if (error.name === 'TypeError' && error.message.includes('fetch')) {
showNetworkErrorMessage('Check your internet connection');
} else {
console.error('Fetch error:', error);
showGeneralErrorMessage();
}
return null;
}
}
Best Practices for Error Handling
Be Specific with Try/Catch Blocks
Keep try blocks as small as possible to isolate the exact source of errors:
// Not ideal: Too much in one try block
try {
const data = JSON.parse(rawData);
processData(data);
saveToDatabase(data);
notifyUser(data);
} catch (error) {
console.error('Something failed:', error);
}
// Better: Multiple specific try/catch blocks
try {
const data = JSON.parse(rawData);
} catch (error) {
console.error('Invalid data format:', error);
return;
}
try {
processData(data);
} catch (error) {
console.error('Processing failed:', error);
logProcessingError(data, error);
}
// Continue with other operations...
Provide Useful Error Messages
Error messages should be clear, actionable, and helpful for both users and developers:
// Poor error message
throw new Error('Failed');
// Better error message
throw new Error('User authentication failed: Invalid password format');
Don't Catch What You Can't Handle
Only catch errors that you can meaningfully respond to:
try {
const result = someRiskyOperation();
} catch (error) {
// If you can't handle it properly, consider re-throwing
console.error('Operation failed:', error);
throw error; // Let a higher level catch block handle it
}
Handle Async Errors Properly
Remember that async/await and Promises require different error handling approaches:
// Using try/catch with async/await
async function fetchUserProfile() {
try {
const response = await fetch('/api/profile');
if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
return await response.json();
} catch (error) {
console.error('Profile fetch failed:', error);
return null;
}
}
// Using .catch() with Promises
fetch('/api/profile')
.then(response => {
if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
return response.json();
})
.then(data => displayProfile(data))
.catch(error => {
console.error('Profile fetch failed:', error);
showErrorMessage();
});
Common Error Handling Patterns
Fallback Values
function getConfigValue(key) {
try {
const config = JSON.parse(localStorage.getItem('appConfig') || '{}');
return config[key] || DEFAULT_CONFIG[key];
} catch (error) {
console.warn(`Could not load config, using defaults: ${error.message}`);
return DEFAULT_CONFIG[key];
}
}
Retry Pattern
async function fetchWithRetry(url, maxRetries = 3) {
let retries = 0;
while (retries < maxRetries) {
try {
const response = await fetch(url);
if (response.ok) return await response.json();
throw new Error(`HTTP error! Status: ${response.status}`);
} catch (error) {
retries++;
console.warn(`Attempt ${retries} failed: ${error.message}`);
if (retries >= maxRetries) throw error;
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, retries - 1)));
}
}
}
Error Boundary Pattern (React)
In React applications, Error Boundaries provide a way to catch and handle errors in components:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Component error:', error, errorInfo);
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div className="error-ui">
<h2>Something went wrong</h2>
<p>{this.state.error.message}</p>
<button onClick={() => this.setState({ hasError: false })}>Try Again</button>
</div>;
}
return this.props.children;
}
}
Practice Activities
Activity 1: Basic Error Handling
Modify the following function to use try/catch to handle potential errors:
function calculateAverage(numbers) {
// The input might not be an array or might contain non-numeric values
// Implement error handling to deal with these cases
const sum = numbers.reduce((acc, val) => acc + val, 0);
return sum / numbers.length;
}
Activity 2: Custom Error Class
Create a custom NetworkError class that extends Error and includes additional details like status code and retry information.
Activity 3: Async Error Handling
Implement robust error handling for a function that:
- Fetches user data from an API
- Processes that data
- Saves the result to localStorage
Consider different types of errors that could occur at each step.
Additional Challenge: Error Logging Service
Implement a simple error logging service that:
- Captures errors with a universal handler
- Formats error details including browser information
- Simulates sending errors to a backend service
- Provides hooks for different error types
Key Takeaways
- Error handling is a critical aspect of writing robust JavaScript applications
- try/catch/finally blocks allow you to handle exceptions gracefully
- JavaScript provides various built-in error types for different error conditions
- Creating custom error classes helps provide context and organize error handling
- Best practices include specific try blocks, meaningful error messages, and proper async error handling
- Common patterns include fallbacks, retries, and error boundaries
Remember: Good error handling is like good insurance - you hope you never need it, but you'll be grateful to have it when something goes wrong!