Advanced TypeScript Error Handling Patterns for Scalable Applications
Learn practical and advanced TypeScript error handling techniques to build scalable and maintainable applications with clear, robust, and type-safe error management.
Error handling is a crucial part of building scalable applications, and TypeScript can help by adding strong type safety to your error management. In this article, we'll explore advanced error handling patterns that are beginner-friendly yet very effective for building maintainable codebases.
A common pattern is using custom error classes instead of throwing generic errors. This makes debugging easier and allows TypeScript to understand error types better.
class AppError extends Error {
public readonly code: number;
constructor(message: string, code: number) {
super(message);
this.code = code;
this.name = this.constructor.name;
// Maintains proper stack trace for where error was thrown (only in V8 engines)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}Using this class, you can throw errors with specific codes, which can help you categorize errors easily later on.
function fetchData(url: string): Promise<string> {
return new Promise((resolve, reject) => {
if (!url.startsWith('https://')) {
reject(new AppError('URL must start with https://', 400));
return;
}
// Simulate fetching data
setTimeout(() => {
resolve('Data from ' + url);
}, 1000);
});
}When consuming functions that might throw errors, use `try-catch` blocks cautiously. Always narrow the possible error types using TypeScript's `instanceof` guard to handle different error shapes correctly.
async function getData() {
try {
const data = await fetchData('http://example.com');
console.log(data);
} catch (error) {
if (error instanceof AppError) {
console.error(`App error (${error.code}): ${error.message}`);
} else if (error instanceof Error) {
console.error(`General error: ${error.message}`);
} else {
console.error('Unknown error', error);
}
}
}Another useful pattern for scalable applications is creating a unified error response object when working with APIs. This object explicitly defines error data, which helps frontend and backend developers handle errors in a consistent way.
interface ApiError {
message: string;
statusCode: number;
details?: any;
}
function createApiError(message: string, statusCode: number, details?: any): ApiError {
return { message, statusCode, details };
}This way, instead of throwing raw errors, you return structured error objects, making it easier to serialize and handle them across services.
async function fetchUser(id: string): Promise<{ name: string } | ApiError> {
if (id === '0') {
return createApiError('User not found', 404);
}
return { name: 'John Doe' };
}
async function main() {
const result = await fetchUser('0');
if ('statusCode' in result) {
console.error(`Error ${result.statusCode}: ${result.message}`);
} else {
console.log('User:', result.name);
}
}Finally, TypeScript’s union types and discriminated unions allow you to model complex error states cleanly and safely.
type NetworkError = { type: 'NETWORK_ERROR'; message: string };
type ValidationError = { type: 'VALIDATION_ERROR'; message: string; field: string };
type AppErrors = NetworkError | ValidationError;
function handleError(error: AppErrors) {
switch (error.type) {
case 'NETWORK_ERROR':
console.error('Network problem:', error.message);
break;
case 'VALIDATION_ERROR':
console.error(`Validation failed on ${error.field}: ${error.message}`);
break;
}
}In summary, advanced TypeScript error handling is about clarity and safety. Use custom errors with classes, unify error return types for APIs, and take advantage of TypeScript’s type system to distinguish errors. This leads to more maintainable and scalable applications.