Comprehensive Error Handling
Overview
Robust error handling is essential for creating a reliable Document Q&A application. This tutorial covers strategies for handling errors in both the frontend and backend components of your application, ensuring a smooth user experience even when things go wrong.
Frontend Error Handling
1. Global Error Boundary
Implement a global error boundary to catch unexpected errors in your React components:
// src/components/ErrorBoundary.js
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// You can log the error to an error reporting service
console.error("Uncaught error:", error, errorInfo);
this.setState({ errorInfo });
}
render() {
if (this.state.hasError) {
return (
<div className="p-6 bg-red-50 rounded-lg">
<h2 className="text-xl font-bold text-red-700 mb-4">Something went wrong</h2>
<p className="mb-4">We're sorry, but an unexpected error occurred.</p>
<details className="bg-white p-4 rounded-md">
<summary className="cursor-pointer font-medium">Error details</summary>
<p className="mt-2 text-red-600">{this.state.error && this.state.error.toString()}</p>
<pre className="mt-2 bg-gray-100 p-2 rounded overflow-x-auto">
{this.state.errorInfo && this.state.errorInfo.componentStack}
</pre>
</details>
<button
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
onClick={() => window.location.reload()}
>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;Wrap your application with the ErrorBoundary component:
// src/app/layout.js
import ErrorBoundary from '../components/ErrorBoundary';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<ErrorBoundary>
{children}
</ErrorBoundary>
</body>
</html>
);
}API Request Error Handling
1. Create a Custom API Client
Implement a custom API client with built-in error handling:
// src/lib/api-client.js
import axios from 'axios';
// Create an axios instance with default config
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || '/api',
timeout: 30000, // 30 seconds
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor
apiClient.interceptors.request.use(
(config) => {
// You can add auth tokens here if needed
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor
apiClient.interceptors.response.use(
(response) => {
return response;
},
(error) => {
// Handle different error types
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
const status = error.response.status;
if (status === 401) {
// Handle unauthorized errors
console.error('Unauthorized access');
// Redirect to login or show auth error
} else if (status === 403) {
// Handle forbidden errors
console.error('Forbidden access');
} else if (status === 404) {
// Handle not found errors
console.error('Resource not found');
} else if (status >= 500) {
// Handle server errors
console.error('Server error occurred');
}
// You can add custom error messages based on the response
const errorMessage = error.response.data?.detail || 'An error occurred';
error.userMessage = errorMessage;
} else if (error.request) {
// The request was made but no response was received
console.error('Network error - no response received');
error.userMessage = 'Unable to connect to the server. Please check your internet connection.';
} else {
// Something happened in setting up the request that triggered an Error
console.error('Request configuration error:', error.message);
error.userMessage = 'An error occurred while setting up the request.';
}
// You can also log errors to a monitoring service here
return Promise.reject(error);
}
);
export default apiClient;2. Using the API Client
Use the custom API client in your components:
// src/components/QuestionInput.js
import React, { useState } from 'react';
import apiClient from '../lib/api-client';
const QuestionInput = ({ documentId }) => {
const [question, setQuestion] = useState('');
const [answer, setAnswer] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
if (!question.trim()) return;
setIsLoading(true);
setError(null);
try {
const response = await apiClient.post('/ask', {
document_id: documentId,
question,
});
setAnswer(response.data.answer);
} catch (err) {
console.error('Question error:', err);
// Use the user-friendly message from our interceptor
setError(err.userMessage || 'Failed to get answer');
} finally {
setIsLoading(false);
}
};
// Component JSX
// ...
};File Upload Error Handling
1. Comprehensive File Validation
Implement thorough file validation before uploading:
// src/components/FileUpload.js
import React, { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import apiClient from '../lib/api-client';
const FileUpload = ({ onDocumentUploaded }) => {
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState(null);
const [progress, setProgress] = useState(0);
// File validation function
const validateFile = (file) => {
// Check file size (e.g., max 10MB)
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
return {
valid: false,
error: `File is too large. Maximum size is ${maxSize / (1024 * 1024)}MB.`
};
}
// Check file type
const allowedTypes = ['application/pdf', 'text/plain'];
if (!allowedTypes.includes(file.type)) {
return {
valid: false,
error: 'Invalid file type. Only PDF and text files are supported.'
};
}
// File is valid
return { valid: true };
};
const onDrop = useCallback(async (acceptedFiles) => {
if (acceptedFiles.length === 0) return;
const file = acceptedFiles[0];
// Validate the file
const validation = validateFile(file);
if (!validation.valid) {
setError(validation.error);
return;
}
setIsUploading(true);
setError(null);
setProgress(0);
// Create form data
const formData = new FormData();
formData.append('file', file);
try {
// Upload with progress tracking
const response = await apiClient.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
setProgress(percentCompleted);
},
});
if (onDocumentUploaded) {
onDocumentUploaded(response.data.document_id);
}
} catch (err) {
console.error('Upload error:', err);
setError(err.userMessage || 'Failed to upload document');
} finally {
setIsUploading(false);
}
}, [onDocumentUploaded]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'application/pdf': ['.pdf'],
'text/plain': ['.txt'],
},
maxFiles: 1,
});
// Component JSX
// ...
};Error Notification System
1. Create a Toast Notification System
Implement a toast notification system for displaying errors:
// src/components/ToastContainer.js
import React, { useState, useEffect } from 'react';
export const ToastContext = React.createContext({
showToast: () => {},
});
export const ToastProvider = ({ children }) => {
const [toasts, setToasts] = useState([]);
const showToast = (message, type = 'error', duration = 5000) => {
const id = Date.now();
setToasts((prevToasts) => [
...prevToasts,
{ id, message, type, duration },
]);
};
const removeToast = (id) => {
setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id));
};
return (
<ToastContext.Provider value={{ showToast }}>
{children}
<div className="fixed bottom-4 right-4 z-50 space-y-2">
{toasts.map((toast) => (
<Toast
key={toast.id}
id={toast.id}
message={toast.message}
type={toast.type}
duration={toast.duration}
onClose={removeToast}
/>
))}
</div>
</ToastContext.Provider>
);
};
const Toast = ({ id, message, type, duration, onClose }) => {
useEffect(() => {
const timer = setTimeout(() => {
onClose(id);
}, duration);
return () => clearTimeout(timer);
}, [id, duration, onClose]);
const bgColor =
type === 'error'
? 'bg-red-100 border-red-400 text-red-700'
: type === 'success'
? 'bg-green-100 border-green-400 text-green-700'
: 'bg-blue-100 border-blue-400 text-blue-700';
return (
<div
className={`px-4 py-3 rounded border ${bgColor} flex items-center justify-between max-w-md animate-fade-in`}
>
<p>{message}</p>
<button
onClick={() => onClose(id)}
className="ml-4 text-gray-500 hover:text-gray-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
);
};
// Hook for using the toast
export const useToast = () => {
const context = React.useContext(ToastContext);
if (context === undefined) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};2. Using the Toast System
Add the ToastProvider to your application and use it in components:
// src/app/layout.js
import { ToastProvider } from '../components/ToastContainer';
import ErrorBoundary from '../components/ErrorBoundary';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<ErrorBoundary>
<ToastProvider>
{children}
</ToastProvider>
</ErrorBoundary>
</body>
</html>
);
}
// In any component:
import { useToast } from '../components/ToastContainer';
const MyComponent = () => {
const { showToast } = useToast();
const handleAction = async () => {
try {
// Do something
} catch (error) {
showToast(error.userMessage || 'An error occurred', 'error');
}
};
// Component JSX
// ...
};Backend Error Handling
1. FastAPI Exception Handlers
Implement custom exception handlers in your FastAPI backend:
# api/main.py
from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Specify your frontend URL in production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Custom exception classes
class DocumentProcessingError(Exception):
def __init__(self, detail: str):
self.detail = detail
class QuestionAnsweringError(Exception):
def __init__(self, detail: str):
self.detail = detail
# Exception handlers
@app.exception_handler(DocumentProcessingError)
async def document_processing_exception_handler(request: Request, exc: DocumentProcessingError):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": exc.detail},
)
@app.exception_handler(QuestionAnsweringError)
async def qa_exception_handler(request: Request, exc: QuestionAnsweringError):
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": exc.detail},
)
# Global exception handler for unexpected errors
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
# Log the error here
print(f"Unexpected error: {str(exc)}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": "An unexpected error occurred. Please try again later."},
)2. Using Custom Exceptions
Use custom exceptions in your API endpoints:
# api/routes/documents.py
from fastapi import APIRouter, UploadFile, File, HTTPException
from ..exceptions import DocumentProcessingError
router = APIRouter()
@router.post("/upload")
async def upload_document(file: UploadFile = File(...)):
try:
# Validate file
if file.content_type not in ["application/pdf", "text/plain"]:
raise DocumentProcessingError("Invalid file type. Only PDF and text files are supported.")
# Process file
# ...
return {"document_id": "123", "message": "Document uploaded successfully"}
except DocumentProcessingError as e:
# This will be caught by the custom exception handler
raise
except Exception as e:
# Log the error
print(f"Error processing document: {str(e)}")
# Convert to our custom exception
raise DocumentProcessingError("Failed to process document")Next Steps
Now that you've implemented comprehensive error handling, you can: