Skip to content

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

CodeDescriptionExample
invalid_typeWrong data typeExpected string, got number
invalid_stringString validation failedInvalid email format
too_smallValue below minimumString too short, number too small
too_bigValue above maximumString too long, number too large
invalid_enum_valueInvalid enum optionMust be 'development', 'production', or 'test'
unrecognized_keysExtra propertiesUnexpected field in object
required_errorMissing required fieldField 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

Next Steps

Released under the MIT License.