Designing Scalable Error Handling Systems in TypeScript for Large-Scale Applications
Learn how to build scalable and maintainable error handling systems in TypeScript for your large-scale applications to improve reliability and developer experience.
Error handling is a crucial part of any application, especially large-scale ones. Properly managing errors makes your application more robust and easier to maintain. In TypeScript, thanks to static typing and advanced language features, you can create scalable error handling systems that provide clear and consistent error information across your codebase.
This article guides you through designing a scalable error handling approach using custom error classes, error codes, and utility functions to standardize error creation and handling.
### Why use custom error classes?
Instead of using the generic `Error` class directly, custom error classes help you communicate the specific error types and make it easier to handle them accordingly. Custom errors also carry additional metadata, like codes, to improve error categorization and logging.
Let's start by creating a base error class that extends the native `Error` class and adds an error code.
class AppError extends Error {
public readonly code: string;
public readonly details?: any;
constructor(code: string, message: string, details?: any) {
super(message);
this.code = code;
this.details = details;
// Set the prototype explicitly.
Object.setPrototypeOf(this, AppError.prototype);
}
}Our `AppError` class has a `code` which is a string representing the type of error, a message for human-readable information, and optional `details` for additional context.
Next, define some specific error types by extending `AppError`. This keeps your errors organized and easy to identify.
class NotFoundError extends AppError {
constructor(message = "Resource not found", details?: any) {
super("NOT_FOUND", message, details);
Object.setPrototypeOf(this, NotFoundError.prototype);
}
}
class ValidationError extends AppError {
constructor(message = "Validation failed", details?: any) {
super("VALIDATION_ERROR", message, details);
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
class DatabaseError extends AppError {
constructor(message = "Database error occurred", details?: any) {
super("DATABASE_ERROR", message, details);
Object.setPrototypeOf(this, DatabaseError.prototype);
}
}With specific error classes, you can handle them more precisely in your application logic or API responses.
To improve scalability, create a centralized error factory or utility function to create errors consistently throughout your app.
function createError(type: string, message?: string, details?: any): AppError {
switch (type) {
case "NOT_FOUND":
return new NotFoundError(message, details);
case "VALIDATION_ERROR":
return new ValidationError(message, details);
case "DATABASE_ERROR":
return new DatabaseError(message, details);
default:
return new AppError("UNKNOWN_ERROR", message || "An unknown error occurred", details);
}
}This factory makes it easy to create errors based on types, keeping your error creation logic in one place.
In your application, when an error occurs, throw one of these errors with meaningful messages and details. Here's an example of error handling in a service function:
async function getUserById(id: string) {
try {
const user = await database.findUser(id);
if (!user) {
throw createError("NOT_FOUND", `User with ID ${id} not found`);
}
return user;
} catch (err) {
if (err instanceof AppError) {
// Handle app-specific errors
console.error(`Error Code: ${err.code}`, err.message);
throw err;
}
// Handle unexpected errors
console.error("Unexpected error", err);
throw createError("UNKNOWN_ERROR", "Unexpected error occurred");
}
}### Benefits of this approach
- **Readability:** Custom classes clarify what kind of errors your app deals with. - **Consistency:** Centralized creation and codes make it easier to handle errors, especially in API responses. - **Extensibility:** Adding new error types is simple and doesn’t require refactoring existing code. - **Type Safety:** TypeScript helps catch errors early and provides autocompletion.
### Summary
By using custom error classes extended from a base class, incorporating error codes, and centralizing error creation, you can build scalable and maintainable error handling systems in TypeScript that are perfect for large-scale applications. This helps create a robust developer experience and makes your app more resilient to errors.