20. Error Handling & Validation

20.1 Client-Side Validation (React)

Amount Validation

File: packages/desktop-app/src/utils/validation.ts

export const validateAmount = (amount: string): {valid: boolean; error?: string} => {
  // Check empty
  if (!amount || amount.trim() === '') {
    return {valid: false, error: 'Amount is required'};
  }
  
  // Check numeric
  const numAmount = parseFloat(amount);
  if (isNaN(numAmount)) {
    return {valid: false, error: 'Amount must be a number'};
  }
  
  // Check positive
  if (numAmount <= 0) {
    return {valid: false, error: 'Amount must be greater than 0'};
  }
  
  // Check maximum (1 trillion)
  if (numAmount > 1_000_000_000_000) {
    return {valid: false, error: 'Amount exceeds maximum limit (1 trillion)'};
  }
  
  // Check decimal places
  const decimalPlaces = (amount.split('.')[1] || '').length;
  if (decimalPlaces > 2) {
    return {valid: false, error: 'Maximum 2 decimal places allowed'};
  }
  
  return {valid: true};
};

Validation Rules

Rule Validation Error Message
Required Not empty "Amount is required"
Numeric Must be a number "Amount must be a number"
Positive > 0 "Amount must be greater than 0"
Maximum = 1 trillion "Amount exceeds maximum limit"
Precision Max 2 decimal places "Maximum 2 decimal places allowed"

File Upload Validation

export const validateUploadFile = (file: File): {valid: boolean; error?: string} => {
  // Check file size (max 10MB)
  const maxSize = 10 * 1024 * 1024; // 10MB
  if (file.size > maxSize) {
    return {valid: false, error: 'File size exceeds 10MB limit'};
  }
  
  // Check file type
  const allowedTypes = [
    'text/csv',
    'application/pdf',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    'image/jpeg',
    'image/png',
    'image/jpg'
  ];
  
  if (!allowedTypes.includes(file.type)) {
    return {
      valid: false,
      error: 'Invalid file type. Allowed: CSV, PDF, Word, Images (JPG/PNG)'
    };
  }
  
  return {valid: true};
};

File Validation Rules

Rule Limit Error Message
File Size = 10 MB "File size exceeds 10MB limit"
File Type CSV, PDF, DOCX, JPG, PNG "Invalid file type. Allowed: CSV, PDF, Word, Images"

UI Validation Display

// Example in CalculatorPage.tsx
const handleCalculate = () => {
  const validation = validateAmount(amount);
  
  if (!validation.valid) {
    toast.error(validation.error); // Show error toast
    return;
  }
  
  // Proceed with calculation...
};

20.2 Server-Side Validation (FastAPI)

Request Models with Validation

File: packages/local-backend/app/models.py

from pydantic import BaseModel, Field, validator
from decimal import Decimal
from typing import Literal

class CalculateRequest(BaseModel):
    amount: Decimal = Field(..., gt=0, le=Decimal('1000000000000'))
    currency: Literal['INR', 'USD', 'EUR', 'GBP']
    mode: Literal['greedy', 'balanced', 'minimize_large', 'minimize_small']
    
    @validator('amount')
    def validate_amount_precision(cls, v):
        # Ensure max 2 decimal places
        if v.as_tuple().exponent < -2:
            raise ValueError('Maximum 2 decimal places allowed')
        return v
    
    class Config:
        json_schema_extra = {
            "example": {
                "amount": 1850.50,
                "currency": "INR",
                "mode": "greedy"
            }
        }

Pydantic Validation Features

Field Validation Implementation
amount gt=0, le=1 trillion, max 2 decimals Field(..., gt=0) + custom validator
currency Must be INR, USD, EUR, or GBP Literal['INR', 'USD', 'EUR', 'GBP']
mode Must be valid optimization mode Literal['greedy', 'balanced', ...]

File Upload Validation

from fastapi import UploadFile, HTTPException
import os

async def validate_upload_file(file: UploadFile):
    # Check file size
    max_size = 10 * 1024 * 1024  # 10MB
    
    # Read file to check size
    contents = await file.read()
    await file.seek(0)  # Reset file pointer
    
    if len(contents) > max_size:
        raise HTTPException(
            status_code=400,
            detail="File size exceeds 10MB limit"
        )
    
    # Check file extension
    allowed_extensions = ['.csv', '.pdf', '.docx', '.jpg', '.jpeg', '.png']
    file_ext = os.path.splitext(file.filename)[1].lower()
    
    if file_ext not in allowed_extensions:
        raise HTTPException(
            status_code=400,
            detail=f"Invalid file type. Allowed: {', '.join(allowed_extensions)}"
        )
    
    return True

20.3 Error Response Format

Standard Error Response

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Amount must be greater than 0",
    "field": "amount",
    "timestamp": "2025-01-15T12:00:00Z"
  }
}

Error Response Structure

Field Type Description
code string Machine-readable error code
message string Human-readable error message
field string (optional) Which field caused the error
timestamp string (ISO 8601) When the error occurred

Error Codes

File: packages/local-backend/app/errors.py

class ErrorCode:
    # Validation Errors (400)
    VALIDATION_ERROR = "VALIDATION_ERROR"
    INVALID_AMOUNT = "INVALID_AMOUNT"
    INVALID_CURRENCY = "INVALID_CURRENCY"
    INVALID_MODE = "INVALID_MODE"
    INVALID_FILE = "INVALID_FILE"
    
    # Not Found (404)
    CALCULATION_NOT_FOUND = "CALCULATION_NOT_FOUND"
    
    # Server Errors (500)
    CALCULATION_FAILED = "CALCULATION_FAILED"
    DATABASE_ERROR = "DATABASE_ERROR"
    OCR_PROCESSING_ERROR = "OCR_PROCESSING_ERROR"

HTTP Status Codes

Status Code Use Case
200 OK Request successful
400 Bad Request Validation errors
404 Not Found Resource not found
422 Unprocessable Entity Pydantic validation errors
500 Internal Server Error Server-side errors

20.4 Exception Hierarchy

File: packages/local-backend/app/exceptions.py

class AppException(Exception):
    """Base application exception."""
    def __init__(self, message: str, code: str):
        self.message = message
        self.code = code
        super().__init__(self.message)

class ValidationException(AppException):
    """Validation error exception."""
    def __init__(self, message: str, field: str = None):
        super().__init__(message, ErrorCode.VALIDATION_ERROR)
        self.field = field

class CalculationException(AppException):
    """Calculation processing error."""
    def __init__(self, message: str):
        super().__init__(message, ErrorCode.CALCULATION_FAILED)

class OCRException(AppException):
    """OCR processing error."""
    def __init__(self, message: str):
        super().__init__(message, ErrorCode.OCR_PROCESSING_ERROR)

Exception Hierarchy Diagram

AppException (Base)
+-- ValidationException
¦   +-- InvalidAmountException
¦   +-- InvalidCurrencyException
¦   +-- InvalidModeException
+-- CalculationException
¦   +-- CalculationFailedException
+-- OCRException
¦   +-- TesseractNotFoundError
¦   +-- OCRParsingError
+-- DatabaseException
    +-- DatabaseConnectionError
    +-- DatabaseQueryError

Usage Example

def calculate_denominations(amount: Decimal, currency: str, mode: str):
    """Calculate with proper exception handling."""
    try:
        # Validate amount
        if amount <= 0:
            raise ValidationException(
                message="Amount must be greater than 0",
                field="amount"
            )
        
        # Validate currency
        if currency not in SUPPORTED_CURRENCIES:
            raise ValidationException(
                message=f"Unsupported currency: {currency}",
                field="currency"
            )
        
        # Perform calculation
        result = engine.calculate(amount, currency, mode)
        return result
        
    except ValidationException:
        raise  # Re-raise validation exceptions
    except Exception as e:
        logger.error(f"Calculation failed: {str(e)}")
        raise CalculationException(f"Calculation failed: {str(e)}")

20.5 Global Exception Handler

File: packages/local-backend/app/main.py

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from datetime import datetime

app = FastAPI()

@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
    """Handle application-specific exceptions."""
    return JSONResponse(
        status_code=400 if isinstance(exc, ValidationException) else 500,
        content={
            "error": {
                "code": exc.code,
                "message": exc.message,
                "field": getattr(exc, 'field', None),
                "timestamp": datetime.utcnow().isoformat()
            }
        }
    )

@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
    """Handle unexpected exceptions."""
    logger.error(f"Unhandled exception: {str(exc)}", exc_info=True)
    return JSONResponse(
        status_code=500,
        content={
            "error": {
                "code": "INTERNAL_SERVER_ERROR",
                "message": "An unexpected error occurred",
                "timestamp": datetime.utcnow().isoformat()
            }
        }
    )

Exception Handler Flow

  1. AppException: Caught by app_exception_handler
    • ValidationException ? HTTP 400
    • Other AppExceptions ? HTTP 500
  2. Unexpected Exception: Caught by general_exception_handler
    • Logged with full stack trace
    • Returns generic error (security)
    • HTTP 500

Error Logging

import logging

logger = logging.getLogger(__name__)

@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
    # Log with full context
    logger.error(
        f"Unhandled exception on {request.method} {request.url.path}",
        exc_info=True,
        extra={
            "method": request.method,
            "path": request.url.path,
            "client": request.client.host,
            "user_agent": request.headers.get("user-agent")
        }
    )
    
    # Return safe error response
    return JSONResponse(
        status_code=500,
        content={
            "error": {
                "code": "INTERNAL_SERVER_ERROR",
                "message": "An unexpected error occurred",
                "timestamp": datetime.utcnow().isoformat()
            }
        }
    )

20.6 Frontend Error Handling

API Error Handler

File: packages/desktop-app/src/utils/api.ts

import { toast } from 'react-toastify';

export const handleApiError = (error: any) => {
  if (error.response) {
    // Server responded with error
    const errorData = error.response.data.error;
    
    if (errorData) {
      toast.error(errorData.message);
    } else {
      toast.error('An error occurred');
    }
  } else if (error.request) {
    // No response from server
    toast.error('Cannot connect to server. Please check if backend is running.');
  } else {
    // Request setup error
    toast.error('An unexpected error occurred');
  }
};

Usage in Component

import { handleApiError } from '@/utils/api';

const CalculatorPage = () => {
  const handleCalculate = async () => {
    try {
      const response = await axios.post('/api/v1/calculate', {
        amount,
        currency,
        mode
      });
      
      setResult(response.data);
      toast.success('Calculation successful!');
    } catch (error) {
      handleApiError(error);  // Centralized error handling
    }
  };
  
  return (
    // Component JSX...
  );
};

Error Display Components

// Error Alert Component
const ErrorAlert = ({ message }: { message: string }) => (
  <div className="bg-red-50 border border-red-200 rounded-lg p-4">
    <div className="flex items-center">
      <AlertCircle className="w-5 h-5 text-red-500 mr-2" />
      <p className="text-red-800">{message}</p>
    </div>
  </div>
);

// Usage
{error && <ErrorAlert message={error} />}

20.7 Validation Summary

Complete Validation Pipeline

Layer 1: Client-Side (TypeScript)

  • Immediate user feedback
  • Prevent unnecessary API calls
  • Better UX with instant validation

Layer 2: Server-Side (Pydantic)

  • Security: Don't trust client
  • Type validation (Decimal, Literal)
  • Custom validators (@validator)

Layer 3: Business Logic (Python)

  • Domain-specific validation
  • Cross-field validation
  • Database constraints

Validation Best Practices

Practice Implementation Benefit
Fail Fast Validate at earliest possible point Better performance, clearer errors
Clear Messages Specific, actionable error messages Better user experience
Consistent Format Standard error response structure Easier client-side handling
Security Never trust client-side validation alone Prevent malicious requests
Logging Log all validation failures Debugging and monitoring
? Section Complete

This section covers comprehensive error handling: Client-side validation (TypeScript validators for amounts/files with immediate feedback), Server-side validation (Pydantic models with Field constraints and custom validators), Standard error response format (code/message/field/timestamp), Error code enumeration (VALIDATION_ERROR, CALCULATION_FAILED, OCR_PROCESSING_ERROR, etc.), Exception hierarchy (AppException base class with ValidationException, CalculationException, OCRException subclasses), Global exception handlers (FastAPI handlers for AppException and general Exception with logging), Frontend error handling (centralized API error handler with toast notifications), Complete validation pipeline (3 layers: client, server, business logic).