
JavaScript Error Handling: Mastering Try/Catch and Custom Exceptions
Understanding try...catch
The try...catch statement allows you to test a block of code for errors and handle those errors gracefully. It is particularly useful for handling asynchronous code, API calls, or any operation that might throw exceptions at runtime.
Here is a basic structure:
try {
// Code that may throw an error
} catch (error) {
// Code to handle the error
}Let's look at a concrete example involving file-like data fetching:
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Failed to fetch data:', error.message);
}If the fetch fails or the JSON parsing errors, the catch block will execute, and the user is alerted without the application crashing.
Custom Exceptions with Error Objects
JavaScript provides the built-in Error object, which can be extended to create custom exceptions. This practice is especially useful when you want to distinguish between different types of errors in your application.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function validateInput(input) {
if (typeof input !== 'string' || input.trim() === '') {
throw new ValidationError("Input must be a non-empty string");
}
return input.trim();
}You can now catch this specific error type:
try {
const userInput = validateInput(null);
console.log('Valid input:', userInput);
} catch (error) {
if (error instanceof ValidationError) {
console.warn('Validation failed:', error.message);
} else {
console.error('Unexpected error:', error.message);
}
}This allows for more granular error handling and improves the clarity of your debugging process.
Best Practices for Error Handling
| Practice | Description |
|---|---|
| \Use try/catch with async operations | Always wrap await or .then() in a try...catch block to handle rejections. |
| \Throw meaningful errors | Use descriptive messages and custom error types to make debugging easier. |
| \Log errors for debugging | Use console.error() or a logging library to capture detailed error information. |
| \Avoid empty catch blocks | Always handle or re-throw the error; otherwise, the error is silently ignored. |
| \Use finally for cleanup | The finally block can be used to execute code regardless of whether an error occurred. |
Here’s how you might apply several of these practices together:
async function fetchData(id) {
try {
const response = await fetch(`https://api.example.com/data/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Data fetched:', data);
} catch (error) {
console.error('Failed to fetch data:', error.message);
} finally {
console.log('Request complete.');
}
}This example handles network errors, logs them, and ensures a cleanup message is always displayed.
Error Handling in Event Listeners
When working with event-driven code, such as DOM events or Node.js streams, it is essential to handle errors within the callback functions:
document.getElementById('myButton').addEventListener('click', () => {
try {
const input = document.getElementById('myInput').value;
const parsed = parseInt(input, 10);
if (isNaN(parsed)) {
throw new Error('Invalid number');
}
console.log('Parsed value:', parsed);
} catch (error) {
alert(`Error: ${error.message}`);
}
});This ensures that user interactions don't break the application flow and provides immediate feedback to the user.
