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: