Error Handling
Comprehensive guide to handling validation errors in the AI-Enhanced TypeScript Template.
ValidationError Class
The template includes a custom ValidationError
class that provides structured error information and formatted messages.
Class Definition
typescript
export class ValidationError extends Error {
public readonly issues: z.ZodIssue[];
constructor(message: string, issues: z.ZodIssue[]) {
super(message);
this.name = 'ValidationError';
this.issues = issues;
}
public getFormattedMessage(): string {
const issueMessages = this.issues.map(issue =>
`${issue.path.join('.')}: ${issue.message}`
).join('; ');
return `${this.message}: ${issueMessages}`;
}
}
Usage Example
typescript
import { ValidationError, AppConfigSchema } from './validation.js';
try {
const config = AppConfigSchema.parse(invalidData);
} catch (error) {
if (error instanceof ValidationError) {
console.log('Formatted error:', error.getFormattedMessage());
// Access individual issues
error.issues.forEach(issue => {
console.log(`Field: ${issue.path.join('.')}`);
console.log(`Error: ${issue.message}`);
console.log(`Code: ${issue.code}`);
});
}
}
Result Type Pattern
All validation functions return a Result
type for consistent error handling:
typescript
type AsyncResult<T> =
| { success: true; data: T }
| { success: false; error: string };
Benefits
- Type-safe error handling - No exceptions to catch
- Explicit error checking - Forces you to handle errors
- Consistent API - All validation functions use the same pattern
- Composable - Easy to chain validation operations
Usage Pattern
typescript
import { validateUserInput } from './validation.js';
function processUser(userData: unknown) {
const result = validateUserInput(userData);
if (!result.success) {
// Handle error case
console.error('Validation failed:', result.error);
return null;
}
// Handle success case - TypeScript knows data is valid
const user = result.data;
console.log(`Processing user: ${user.name}`);
return user;
}
Error Types and Codes
Zod provides specific error codes for different validation failures:
Common Error Codes
Code | Description | Example |
---|---|---|
invalid_type | Wrong data type | Expected string, got number |
invalid_string | String validation failed | Invalid email format |
too_small | Value below minimum | String too short, number too small |
too_big | Value above maximum | String too long, number too large |
invalid_enum_value | Invalid enum option | Must be 'development', 'production', or 'test' |
unrecognized_keys | Extra properties | Unexpected field in object |
required_error | Missing required field | Field is required |
Handling Specific Error Types
typescript
import { z } from 'zod';
function handleValidationError(error: z.ZodError) {
error.issues.forEach(issue => {
switch (issue.code) {
case 'invalid_type':
console.log(`Expected ${issue.expected}, got ${issue.received} at ${issue.path.join('.')}`);
break;
case 'invalid_string':
if (issue.validation === 'email') {
console.log(`Invalid email format at ${issue.path.join('.')}`);
}
break;
case 'too_small':
console.log(`Value too small at ${issue.path.join('.')}: minimum is ${issue.minimum}`);
break;
case 'too_big':
console.log(`Value too large at ${issue.path.join('.')}: maximum is ${issue.maximum}`);
break;
default:
console.log(`Validation error at ${issue.path.join('.')}: ${issue.message}`);
}
});
}
API Error Responses
Express.js Error Handling
typescript
import { Request, Response, NextFunction } from 'express';
import { ValidationError } from './validation.js';
// Global error handler
function errorHandler(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ValidationError) {
return res.status(400).json({
error: 'Validation failed',
message: error.message,
details: error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
code: issue.code
}))
});
}
// Handle other errors...
res.status(500).json({ error: 'Internal server error' });
}
app.use(errorHandler);
Structured Error Response
typescript
interface ValidationErrorResponse {
error: 'validation_failed';
message: string;
details: Array<{
field: string;
message: string;
code: string;
received?: unknown;
expected?: string;
}>;
}
function createValidationErrorResponse(error: ValidationError): ValidationErrorResponse {
return {
error: 'validation_failed',
message: error.message,
details: error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
code: issue.code,
received: 'received' in issue ? issue.received : undefined,
expected: 'expected' in issue ? issue.expected : undefined
}))
};
}
Client-Side Error Handling
React Error Display
typescript
import React from 'react';
interface ValidationErrorDisplayProps {
error: string | null;
}
function ValidationErrorDisplay({ error }: ValidationErrorDisplayProps) {
if (!error) return null;
// Parse structured error if it's JSON
try {
const parsedError = JSON.parse(error);
if (parsedError.error === 'validation_failed') {
return (
<div className="validation-errors">
<h4>Validation Errors:</h4>
<ul>
{parsedError.details.map((detail, index) => (
<li key={index}>
<strong>{detail.field}:</strong> {detail.message}
</li>
))}
</ul>
</div>
);
}
} catch {
// Fall back to simple error display
}
return <div className="error">{error}</div>;
}
Form Field Errors
typescript
function extractFieldErrors(errorString: string): Record<string, string> {
try {
const parsedError = JSON.parse(errorString);
if (parsedError.error === 'validation_failed') {
const fieldErrors: Record<string, string> = {};
parsedError.details.forEach(detail => {
fieldErrors[detail.field] = detail.message;
});
return fieldErrors;
}
} catch {
// Return empty object if parsing fails
}
return {};
}
// Usage in form component
function UserForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = async (formData) => {
try {
await submitUser(formData);
setErrors({});
} catch (error) {
const fieldErrors = extractFieldErrors(error.message);
setErrors(fieldErrors);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="name" />
{errors.name && <span className="error">{errors.name}</span>}
<input name="email" />
{errors.email && <span className="error">{errors.email}</span>}
</form>
);
}
Logging Validation Errors
Winston Integration
typescript
import { logger } from './logger.js';
import { ValidationError } from './validation.js';
function logValidationError(error: ValidationError, context?: string) {
logger.error('Validation failed', {
context,
message: error.message,
issues: error.issues.map(issue => ({
path: issue.path.join('.'),
message: issue.message,
code: issue.code
}))
});
}
// Usage
try {
const config = validateAppConfig(configData);
} catch (error) {
if (error instanceof ValidationError) {
logValidationError(error, 'application startup');
}
}
Structured Logging
typescript
import { loggerUtils } from './logger.js';
function handleConfigValidation(configData: unknown) {
const result = validateAppConfig(configData);
if (result.success) {
loggerUtils.logValidationSuccess(
logger,
'Configuration validation passed',
{ config: result.data.app }
);
return result.data;
} else {
loggerUtils.logValidationFailure(
logger,
'Configuration validation failed',
result.error
);
throw new Error(`Invalid configuration: ${result.error}`);
}
}
Error Recovery Strategies
Graceful Degradation
typescript
function loadConfigWithFallback(primaryPath: string, fallbackPath: string) {
// Try primary config
const primaryResult = ConfigManager.validateConfigFile(primaryPath);
if (primaryResult.success) {
logger.info(`Loaded configuration from ${primaryPath}`);
return primaryResult.data;
}
logger.warn(`Primary config failed: ${primaryResult.error}`);
// Try fallback config
const fallbackResult = ConfigManager.validateConfigFile(fallbackPath);
if (fallbackResult.success) {
logger.info(`Loaded fallback configuration from ${fallbackPath}`);
return fallbackResult.data;
}
// Both failed - use defaults
logger.error('All configurations failed, using defaults');
return getDefaultConfig();
}
Partial Validation
typescript
// Validate only critical fields and warn about others
function validateConfigPartial(configData: unknown) {
const criticalFields = AppConfigSchema.pick({
app: true,
server: true
});
const criticalResult = validateConfig(criticalFields, configData);
if (!criticalResult.success) {
throw new Error(`Critical configuration invalid: ${criticalResult.error}`);
}
// Validate full config and warn about non-critical issues
const fullResult = validateAppConfig(configData);
if (!fullResult.success) {
logger.warn(`Non-critical configuration issues: ${fullResult.error}`);
}
return criticalResult.data;
}
Best Practices
✅ Do
- Always handle validation errors - Never ignore failed validation
- Provide specific error messages - Help users understand what went wrong
- Log validation failures - Track validation issues for debugging
- Use structured error responses - Make errors machine-readable
- Implement graceful degradation - Provide fallbacks when possible
❌ Don't
- Expose internal error details - Sanitize errors for public APIs
- Ignore validation in production - Validation is especially important in production
- Use generic error messages - Be specific about what failed
- Throw exceptions for expected failures - Use Result types instead
- Skip error logging - Always log validation failures for debugging