Advanced TypeScript Error Handling Patterns for Scalable Enterprise Applications
Learn advanced error handling patterns in TypeScript to build scalable and maintainable enterprise-grade applications with cleaner and safer error management.
Error handling is a crucial aspect of building robust and scalable applications, especially in enterprise environments. TypeScript provides powerful features that help us handle errors more effectively beyond the basic try-catch blocks and the built-in Error object. In this article, we'll explore advanced error handling patterns in TypeScript that improve code maintainability, readability, and reliability.
### Why Advanced Error Handling?
In large applications, simply throwing and catching generic errors is not enough. You want to categorize, enrich, and handle errors in ways that provide meaningful information to developers and even end users. This helps with debugging, logging, retry mechanisms, and presenting user-friendly messages.
### 1. Using Custom Error Classes
Instead of using the generic Error object, customize your own error classes to provide specific context about an error.
class DatabaseError extends Error {
public code: number;
constructor(message: string, code: number) {
super(message);
this.code = code;
this.name = 'DatabaseError';
Object.setPrototypeOf(this, DatabaseError.prototype); // Fix prototype chain
}
}
// Usage:
try {
throw new DatabaseError('Connection Timeout', 504);
} catch (err) {
if (err instanceof DatabaseError) {
console.error(`DB Error ${err.code}: ${err.message}`);
} else {
console.error('Unknown error:', err);
}
}### 2. Using Result Types Instead of Exceptions
Another pattern popular in functional programming is using a Result type which explicitly models success or failure, avoiding exceptions for flow control.
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
function fetchData(id: string): Result<string, string> {
if (id === 'valid') {
return { ok: true, value: 'data for valid id' };
}
return { ok: false, error: 'ID not found' };
}
const result = fetchData('invalid');
if (result.ok) {
console.log('Success:', result.value);
} else {
console.error('Failure:', result.error);
}This pattern makes error handling explicit and forces the caller to handle both success and failure.
### 3. Error Wrapping and Chaining
To maintain context about where an error originates, you can wrap errors inside custom error classes. This helps when errors bubble up through layers.
class AppError extends Error {
originalError?: Error;
constructor(message: string, originalError?: Error) {
super(message);
this.name = 'AppError';
this.originalError = originalError;
Object.setPrototypeOf(this, AppError.prototype);
}
}
function apiCall() {
try {
throw new Error('Low-level API failure');
} catch (err) {
throw new AppError('API call failed', err instanceof Error ? err : undefined);
}
}
try {
apiCall();
} catch (err) {
if (err instanceof AppError && err.originalError) {
console.error('Cause:', err.originalError.message);
}
console.error('Error:', err.message);
}### 4. Creating Typed Error Responses for APIs
When writing REST or GraphQL APIs, explicitly typing your error responses helps frontend developers handle errors correctly.
interface ApiError {
code: number;
message: string;
details?: any;
}
interface ApiResponse<T> {
data?: T;
error?: ApiError;
}
async function getUser(id: string): Promise<ApiResponse<{ name: string }>> {
if (id === '123') {
return { data: { name: 'Alice' } };
}
return { error: { code: 404, message: 'User not found' } };
}
getUser('999').then(response => {
if (response.error) {
console.error(`API Error ${response.error.code}: ${response.error.message}`);
} else {
console.log('User data:', response.data);
}
});### 5. Using Utility Libraries for Better Error Handling
Libraries like `fp-ts` provide functional types like Either and Option to handle errors and optional values safely and declaratively.
For beginners, focus on mastering the above TypeScript native patterns first before moving to advanced functional libraries.
### Summary
Advanced error handling in TypeScript means using custom errors, result types, error wrapping, typed API error responses, and eventually functional programming libraries. These patterns help create scalable, maintainable, and reliable enterprise applications by making errors more descriptive and easier to manage.