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
- AppException: Caught by
app_exception_handler- ValidationException ? HTTP 400
- Other AppExceptions ? HTTP 500
- 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 |
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).