Error handling is a critical aspect of building robust TypeScript applications. While TypeScript’s static type system helps prevent many compile-time errors, runtime exceptions still need to be properly managed. In this comprehensive guide, we’ll explore TypeScript’s error handling mechanisms and best practices for creating more reliable applications.
Error handling in TypeScript builds upon JavaScript’s error handling capabilities while adding type safety and additional features. Let’s dive into the essential concepts and techniques you need to know.
Table of Contents
- Understanding TypeScript Errors
- The try-catch Block
- Custom Error Types
- Error Type Guards
- Async Error Handling
- Error Handling Best Practices
- Logging and Monitoring
- Integration with Express
- Conclusion
Understanding TypeScript Errors
TypeScript errors can be broadly categorized into two types:
- Compile-time errors (caught by the TypeScript compiler)
- Runtime errors (occurring during program execution)
While TypeScript’s type system helps prevent many compile-time errors, runtime errors require explicit handling in your code.
The try-catch Block
The fundamental mechanism for handling runtime errors in TypeScript is the try-catch block. Here’s how to use it effectively:
try {
// Code that might throw an error
throw new Error('Something went wrong!');
} catch (error) {
// Handle the error
if (error instanceof Error) {
console.error(error.message);
}
}
Code language: JavaScript (javascript)
Custom Error Types
TypeScript allows you to create custom error types by extending the built-in Error class:
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
class DatabaseError extends Error {
constructor(message: string) {
super(message);
this.name = 'DatabaseError';
}
}
Code language: JavaScript (javascript)
Error Type Guards
Type guards help narrow down the type of an error for better handling:
function isValidationError(error: unknown): error is ValidationError {
return error instanceof ValidationError;
}
function isDatabaseError(error: unknown): error is DatabaseError {
return error instanceof DatabaseError;
}
try {
// Some operation that might throw different types of errors
} catch (error) {
if (isValidationError(error)) {
// Handle validation error
} else if (isDatabaseError(error)) {
// Handle database error
} else {
// Handle unknown error
}
}
Code language: JavaScript (javascript)
Async Error Handling
When working with asynchronous operations, use async/await with try-catch:
async function fetchData(): Promise<string> {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.text();
} catch (error) {
if (error instanceof Error) {
console.error('Failed to fetch data:', error.message);
}
throw error; // Re-throw the error for the caller to handle
}
}
Code language: JavaScript (javascript)
Error Handling Best Practices
1. Be Specific with Error Types
Create specific error types for different scenarios:
class NetworkError extends Error {
constructor(message: string, public statusCode: number) {
super(message);
this.name = 'NetworkError';
}
}
Code language: JavaScript (javascript)
2. Implement Error Boundaries
Create error boundary components in React applications:
class ErrorBoundary extends React.Component<{}, { hasError: boolean }> {
state = { hasError: false };
static getDerivedStateFromError(error: Error) {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
Code language: JavaScript (javascript)
3. Use Result Types
Implement a Result type for operations that might fail:
type Result<T, E = Error> = {
success: true;
data: T;
} | {
success: false;
error: E;
};
function divide(a: number, b: number): Result<number> {
if (b === 0) {
return {
success: false,
error: new Error('Division by zero')
};
}
return {
success: true,
data: a / b
};
}
Code language: JavaScript (javascript)
Logging and Monitoring
Implement proper error logging for production environments:
class Logger {
static error(error: Error, context?: Record<string, unknown>) {
console.error({
message: error.message,
stack: error.stack,
context,
timestamp: new Date().toISOString()
});
}
}
try {
throw new Error('Critical error');
} catch (error) {
if (error instanceof Error) {
Logger.error(error, { component: 'UserService' });
}
}
Code language: JavaScript (javascript)
Integration with Express
For Express applications, create a centralized error handling middleware:
import { Request, Response, NextFunction } from 'express';
interface CustomError extends Error {
statusCode?: number;
}
function errorHandler(
error: CustomError,
req: Request,
res: Response,
next: NextFunction
) {
const statusCode = error.statusCode || 500;
const message = error.message || 'Internal Server Error';
res.status(statusCode).json({
success: false,
message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
});
}
Code language: JavaScript (javascript)
Conclusion
Effective error handling is crucial for building reliable TypeScript applications. By following these patterns and best practices, you can create more robust applications that gracefully handle runtime errors and provide better user experiences.
Remember to:
- Use specific error types for different scenarios
- Implement proper logging and monitoring
- Handle async errors appropriately
- Use type guards to narrow error types
- Consider implementing error boundaries for React applications
For more advanced TypeScript topics, check out our guide on TypeScript Type Guards and TypeScript Generics.