JavaScript Custom Errors
When building robust applications, proper error handling is essential. JavaScript provides built-in error types like Error
, SyntaxError
, and TypeError
, but sometimes you need more specific errors tailored to your application's needs. This is where custom errors come into play.
Introduction to Custom Errors
Custom errors allow you to create application-specific error types that clearly communicate what went wrong in your code. They make debugging easier and help provide meaningful feedback to users or developers.
By creating custom error classes that extend the native Error
class, you can:
- Define domain-specific errors
- Include additional properties relevant to your application
- Create hierarchies of related errors
- Improve error handling with more precise
catch
blocks
Creating a Basic Custom Error
Let's start by creating a simple custom error in JavaScript:
class CustomError extends Error {
constructor(message) {
super(message);
this.name = 'CustomError';
}
}
// Using our custom error
try {
throw new CustomError('This is a custom error message');
} catch (error) {
console.log(error.name); // Output: CustomError
console.log(error.message); // Output: This is a custom error message
}
When extending the Error
class, we:
- Call
super(message)
to invoke the parent class constructor - Set the
name
property to identify our custom error type
Adding Custom Properties
One of the main advantages of custom errors is the ability to include additional context:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.date = new Date();
}
}
try {
throw new ValidationError('Invalid value', 'email');
} catch (error) {
console.log(error.name); // Output: ValidationError
console.log(error.message); // Output: Invalid value
console.log(error.field); // Output: email
console.log(error.date); // Output: [current date and time]
}
In this example, the ValidationError
includes:
- The field that failed validation
- The timestamp when the error occurred
Creating an Error Hierarchy
For complex applications, you might want to create a hierarchy of error types:
// Base application error
class AppError extends Error {
constructor(message) {
super(message);
this.name = 'AppError';
}
}
// More specific errors
class NetworkError extends AppError {
constructor(message, statusCode) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
}
}
class DatabaseError extends AppError {
constructor(message, query) {
super(message);
this.name = 'DatabaseError';
this.query = query;
}
}
// Using these errors
try {
// Simulate an API call failure
const response = {status: 404};
if (response.status === 404) {
throw new NetworkError('Resource not found', 404);
}
} catch (error) {
if (error instanceof NetworkError) {
console.log(`Network error (${error.statusCode}): ${error.message}`);
// Output: Network error (404): Resource not found
} else if (error instanceof AppError) {
console.log(`Application error: ${error.message}`);
} else {
console.log(`Unexpected error: ${error.message}`);
}
}
This hierarchical approach allows you to catch errors at different levels of specificity.
Cross-Browser Compatibility
For older browsers that might not support ES6 classes, you can use a function-based approach:
function LegacyCustomError(message) {
// Create an error object
const error = new Error(message);
// Set the name property
error.name = 'LegacyCustomError';
// Capture the stack trace (removes this function from the stack)
if (Error.captureStackTrace) {
Error.captureStackTrace(error, LegacyCustomError);
}
return error;
}
try {
throw LegacyCustomError('This works in older browsers too');
} catch (error) {
console.log(error.name); // Output: LegacyCustomError
console.log(error.message); // Output: This works in older browsers too
}
The Error.captureStackTrace
method (when available) ensures that the stack trace starts at the point where the error is thrown, not inside the error constructor.
Real-World Example: Form Validation
Let's see how custom errors can improve a form validation system:
// Define a hierarchy of validation errors
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
class RequiredFieldError extends ValidationError {
constructor(field) {
super(`The ${field} field is required`);
this.field = field;
}
}
class InvalidFormatError extends ValidationError {
constructor(field, format) {
super(`The ${field} field must be in ${format} format`);
this.field = field;
this.format = format;
}
}
// Form validation function
function validateForm(data) {
// Check required fields
if (!data.email) {
throw new RequiredFieldError('email');
}
// Check email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
throw new InvalidFormatError('email', '[email protected]');
}
// Check password
if (!data.password) {
throw new RequiredFieldError('password');
}
if (data.password.length < 8) {
throw new ValidationError('Password must be at least 8 characters');
}
// If we got here, the form is valid
return true;
}
// Using the validation
try {
const formData = {
email: 'invalid-email',
password: '123'
};
validateForm(formData);
console.log('Form is valid!');
} catch (error) {
if (error instanceof RequiredFieldError) {
console.log(`Please fill in the ${error.field} field`);
} else if (error instanceof InvalidFormatError) {
console.log(`${error.message}. Example: ${error.format}`);
// Output: The email field must be in [email protected] format. Example: [email protected]
} else if (error instanceof ValidationError) {
console.log(error.message);
// Output: Password must be at least 8 characters
} else {
console.log('An unexpected error occurred:', error);
}
}
This example shows how custom error types can make error handling more specific and user-friendly.
Serializing Custom Errors
When working with APIs or logging systems, you might need to serialize your errors. Since JavaScript's built-in JSON.stringify()
doesn't include the error's name or stack by default, custom errors need special handling:
class ApiError extends Error {
constructor(message, code, details = {}) {
super(message);
this.name = 'ApiError';
this.code = code;
this.details = details;
}
// Create a method to properly serialize the error
toJSON() {
return {
name: this.name,
message: this.message,
code: this.code,
details: this.details,
stack: this.stack
};
}
}
const error = new ApiError('API rate limit exceeded', 'RATE_LIMIT', {
limit: 100,
period: '1 hour'
});
console.log(JSON.stringify(error));
// Output: {"name":"ApiError","message":"API rate limit exceeded","code":"RATE_LIMIT","details":{"limit":100,"period":"1 hour"},"stack":"..."}
The toJSON
method ensures all important error information is preserved when serializing.
Summary
Custom errors in JavaScript provide a powerful way to handle application-specific error scenarios. By extending the native Error
class, you can create detailed error types that improve debugging and user experience.
Key benefits of custom errors:
- More descriptive error types for specific situations
- Additional context through custom properties
- Error hierarchies for more flexible error handling
- Better developer and user experiences through precise error messages
Next time you find yourself repeatedly checking error messages with string comparisons, consider if a custom error type would make your code cleaner and more maintainable.
Exercises
- Create a custom
TimeoutError
class that includes information about which operation timed out and the time limit. - Design a hierarchy of custom errors for a shopping cart application (e.g.,
OutOfStockError
,InvalidCouponError
). - Enhance the form validation example to include more validation types and user-friendly error messages.
- Create a custom error handler that formats and logs different types of errors appropriately.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)