TypeScript error handling patterns
- 02 Jul 2025 |
- 01 Min read
Error handling in TypeScript can be tricky. After building payment systems where errors can cost money, I've learned that good error handling is about more than try/catch-it's about creating a robust error handling strategy.
The Problem
JavaScript's error handling is limited:
- No discriminated unions for errors
- Easy to forget error cases
- No type safety for error types
- Inconsistent error patterns
Pattern 1: Result Type
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function processPayment(amount: number): Result<Payment, PaymentError> {
if (amount <= 0) {
return { success: false, error: new PaymentError('Invalid amount') };
}
// Process payment...
return { success: true, data: payment };
}
// Usage
const result = processPayment(100);
if (result.success) {
console.log(result.data); // TypeScript knows this is Payment
} else {
console.error(result.error); // TypeScript knows this is PaymentError
}
Pattern 2: Custom Error Classes
class PaymentError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 400
) {
super(message);
this.name = 'PaymentError';
}
}
class InsufficientFundsError extends PaymentError {
constructor(public balance: number, public requested: number) {
super(
`Insufficient funds: ${balance} < ${requested}`,
'INSUFFICIENT_FUNDS',
402
);
}
}
Pattern 3: Error Handling Middleware
async function errorHandler(
error: unknown,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof PaymentError) {
return res.status(error.statusCode).json({
error: {
code: error.code,
message: error.message,
},
});
}
// Log unexpected errors
console.error('Unexpected error:', error);
return res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
},
});
}
Pattern 4: Async Error Wrapper
function asyncHandler<T extends RequestHandler>(
fn: T
): RequestHandler {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Usage
app.post('/payments', asyncHandler(async (req, res) => {
const payment = await processPayment(req.body);
res.json(payment);
}));
Best Practices
- Use specific error types
- Include context in errors
- Log errors appropriately
- Don't expose internals to users
- Handle errors at the right level
- Use type guards for error checking
"Good error handling makes debugging easier and improves user experience."