# Frontend Exception Handling Guide ## Understanding Your API's Exception Structure Your API returns consistent error responses with this structure: ```json { "error_code": "PRODUCT_NOT_FOUND", "message": "MarketplaceProduct with ID 'ABC123' not found", "status_code": 404, "field": "marketplace_product_id", "details": { "resource_type": "MarketplaceProduct", "identifier": "ABC123" } } ``` ## Frontend Error Handling Strategy ### 1. HTTP Client Configuration Configure your HTTP client to handle error responses consistently: #### Axios Example ```javascript // api/client.js import axios from 'axios'; const apiClient = axios.create({ baseURL: 'https://your-api.com/api/v1', timeout: 10000, }); // Response interceptor for error handling apiClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.data) { // Transform API error to standardized format const apiError = { errorCode: error.response.data.error_code, message: error.response.data.message, statusCode: error.response.data.status_code, field: error.response.data.field, details: error.response.data.details, originalError: error }; throw apiError; } // Handle network errors if (error.code === 'ECONNABORTED') { throw { errorCode: 'NETWORK_TIMEOUT', message: 'Request timed out. Please try again.', statusCode: 408 }; } throw { errorCode: 'NETWORK_ERROR', message: 'Network error. Please check your connection.', statusCode: 0 }; } ); export default apiClient; ``` #### Fetch Example ```javascript // api/client.js class ApiClient { constructor(baseURL) { this.baseURL = baseURL; } async request(endpoint, options = {}) { try { const response = await fetch(`${this.baseURL}${endpoint}`, { headers: { 'Content-Type': 'application/json', ...options.headers, }, ...options, }); const data = await response.json(); if (!response.ok) { throw { errorCode: data.error_code, message: data.message, statusCode: data.status_code, field: data.field, details: data.details, }; } return data; } catch (error) { if (error.errorCode) { throw error; // Re-throw API errors } // Handle network errors throw { errorCode: 'NETWORK_ERROR', message: 'Failed to connect to server', statusCode: 0 }; } } } export const apiClient = new ApiClient('https://your-api.com/api/v1'); ``` ### 2. Error Code Mapping Create mappings for user-friendly messages: ```javascript // constants/errorMessages.js export const ERROR_MESSAGES = { // Authentication errors INVALID_CREDENTIALS: 'Invalid username or password. Please try again.', TOKEN_EXPIRED: 'Your session has expired. Please log in again.', USER_NOT_ACTIVE: 'Your account has been deactivated. Contact support.', // MarketplaceProduct errors PRODUCT_NOT_FOUND: 'MarketplaceProduct not found. It may have been removed.', PRODUCT_ALREADY_EXISTS: 'A product with this ID already exists.', INVALID_PRODUCT_DATA: 'Please check the product information and try again.', // Inventory errors INSUFFICIENT_INVENTORY: 'Not enough inventory available for this operation.', INVENTORY_NOT_FOUND: 'No inventory information found for this product.', NEGATIVE_INVENTORY_NOT_ALLOWED: 'Inventory quantity cannot be negative.', // Shop errors SHOP_NOT_FOUND: 'Shop not found or no longer available.', UNAUTHORIZED_SHOP_ACCESS: 'You do not have permission to access this shop.', SHOP_ALREADY_EXISTS: 'A shop with this code already exists.', MAX_SHOPS_REACHED: 'You have reached the maximum number of shops allowed.', // Import errors IMPORT_JOB_NOT_FOUND: 'Import job not found.', IMPORT_JOB_CANNOT_BE_CANCELLED: 'This import job cannot be cancelled at this time.', MARKETPLACE_CONNECTION_FAILED: 'Failed to connect to marketplace. Please try again.', // Generic fallbacks VALIDATION_ERROR: 'Please check your input and try again.', NETWORK_ERROR: 'Connection error. Please check your internet connection.', NETWORK_TIMEOUT: 'Request timed out. Please try again.', INTERNAL_SERVER_ERROR: 'Something went wrong. Please try again later.', }; export const getErrorMessage = (errorCode, fallbackMessage = null) => { return ERROR_MESSAGES[errorCode] || fallbackMessage || 'An unexpected error occurred.'; }; ``` ### 3. Component Error Handling #### React Hook for Error Handling ```javascript // hooks/useApiError.js import { useState } from 'react'; import { getErrorMessage } from '../constants/errorMessages'; export const useApiError = () => { const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const handleApiCall = async (apiCall) => { setIsLoading(true); setError(null); try { const result = await apiCall(); setIsLoading(false); return result; } catch (apiError) { setIsLoading(false); setError({ code: apiError.errorCode, message: getErrorMessage(apiError.errorCode, apiError.message), field: apiError.field, details: apiError.details }); throw apiError; // Re-throw for component-specific handling } }; const clearError = () => setError(null); return { error, isLoading, handleApiCall, clearError }; }; ``` #### Form Error Handling ```javascript // components/ProductForm.jsx import React, { useState } from 'react'; import { useApiError } from '../hooks/useApiError'; import { createProduct } from '../api/products'; const ProductForm = () => { const [formData, setFormData] = useState({ product_id: '', name: '', price: '' }); const [fieldErrors, setFieldErrors] = useState({}); const { error, isLoading, handleApiCall, clearError } = useApiError(); const handleSubmit = async (e) => { e.preventDefault(); setFieldErrors({}); clearError(); try { await handleApiCall(() => createProduct(formData)); // Success handling alert('MarketplaceProduct created successfully!'); setFormData({ product_id: '', name: '', price: '' }); } catch (apiError) { // Handle field-specific errors if (apiError.field) { setFieldErrors({ [apiError.field]: apiError.message }); } // Handle specific error codes switch (apiError.errorCode) { case 'PRODUCT_ALREADY_EXISTS': setFieldErrors({ product_id: 'This product ID is already taken' }); break; case 'INVALID_PRODUCT_DATA': if (apiError.field) { setFieldErrors({ [apiError.field]: apiError.message }); } break; default: // Generic error is handled by useApiError hook break; } } }; return (
{error && !error.field && (
{error.message}
)}
setFormData({...formData, product_id: e.target.value})} className={fieldErrors.product_id ? 'error' : ''} /> {fieldErrors.product_id && ( {fieldErrors.product_id} )}
setFormData({...formData, name: e.target.value})} className={fieldErrors.name ? 'error' : ''} /> {fieldErrors.name && ( {fieldErrors.name} )}
); }; ``` ### 4. Global Error Handler #### React Error Boundary ```javascript // components/ErrorBoundary.jsx import React from 'react'; class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, errorInfo) { console.error('Error caught by boundary:', error, errorInfo); // Log to error reporting service if (window.errorReporting) { window.errorReporting.captureException(error, { extra: errorInfo }); } } render() { if (this.state.hasError) { return (

Something went wrong

We're sorry, but something unexpected happened.

); } return this.props.children; } } ``` #### Global Toast Notifications ```javascript // utils/notifications.js class NotificationManager { constructor() { this.notifications = []; this.listeners = []; } subscribe(callback) { this.listeners.push(callback); return () => { this.listeners = this.listeners.filter(l => l !== callback); }; } notify(type, message, options = {}) { const notification = { id: Date.now(), type, // 'success', 'error', 'warning', 'info' message, duration: options.duration || 5000, ...options }; this.notifications.push(notification); this.listeners.forEach(callback => callback(this.notifications)); if (notification.duration > 0) { setTimeout(() => this.remove(notification.id), notification.duration); } } notifyError(apiError) { const message = getErrorMessage(apiError.errorCode, apiError.message); this.notify('error', message, { errorCode: apiError.errorCode, details: apiError.details }); } remove(id) { this.notifications = this.notifications.filter(n => n.id !== id); this.listeners.forEach(callback => callback(this.notifications)); } } export const notificationManager = new NotificationManager(); ``` ### 5. Specific Error Handling Patterns #### Authentication Errors ```javascript // api/auth.js import { apiClient } from './client'; import { notificationManager } from '../utils/notifications'; export const login = async (credentials) => { try { const response = await apiClient.post('/auth/login', credentials); localStorage.setItem('token', response.access_token); return response; } catch (error) { switch (error.errorCode) { case 'INVALID_CREDENTIALS': notificationManager.notify('error', 'Invalid username or password'); break; case 'USER_NOT_ACTIVE': notificationManager.notify('error', 'Account deactivated. Contact support.'); break; default: notificationManager.notifyError(error); } throw error; } }; // Handle token expiration globally apiClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.data?.error_code === 'TOKEN_EXPIRED') { localStorage.removeItem('token'); window.location.href = '/login'; notificationManager.notify('warning', 'Session expired. Please log in again.'); } throw error; } ); ``` #### Inventory Management Errors ```javascript // components/InventoryManager.jsx const handleInventoryUpdate = async (gtin, location, quantity) => { try { await updateInventory(gtin, location, quantity); notificationManager.notify('success', 'Inventory updated successfully'); } catch (error) { switch (error.errorCode) { case 'INSUFFICIENT_INVENTORY': const { available_quantity, requested_quantity } = error.details; notificationManager.notify('error', `Cannot remove ${requested_quantity} items. Only ${available_quantity} available.` ); break; case 'INVENTORY_NOT_FOUND': notificationManager.notify('error', 'No inventory record found for this product'); break; case 'NEGATIVE_INVENTORY_NOT_ALLOWED': notificationManager.notify('error', 'Inventory quantity cannot be negative'); break; default: notificationManager.notifyError(error); } } }; ``` #### Import Job Monitoring ```javascript // components/ImportJobStatus.jsx const ImportJobStatus = ({ jobId }) => { const [job, setJob] = useState(null); const { error, isLoading, handleApiCall } = useApiError(); const fetchJobStatus = useCallback(async () => { try { const jobData = await handleApiCall(() => getImportJobStatus(jobId)); setJob(jobData); } catch (error) { switch (error.errorCode) { case 'IMPORT_JOB_NOT_FOUND': notificationManager.notify('error', 'Import job not found or has been deleted'); break; case 'IMPORT_JOB_NOT_OWNED': notificationManager.notify('error', 'You do not have access to this import job'); break; default: notificationManager.notifyError(error); } } }, [jobId, handleApiCall]); const cancelJob = async () => { try { await handleApiCall(() => cancelImportJob(jobId)); notificationManager.notify('success', 'Import job cancelled successfully'); fetchJobStatus(); // Refresh status } catch (error) { switch (error.errorCode) { case 'IMPORT_JOB_CANNOT_BE_CANCELLED': const { current_status } = error.details; notificationManager.notify('error', `Cannot cancel job in ${current_status} status` ); break; default: notificationManager.notifyError(error); } } }; return (
{/* Job status UI */}
); }; ``` ### 6. Error Logging and Monitoring ```javascript // utils/errorReporting.js export const logError = (error, context = {}) => { const errorData = { timestamp: new Date().toISOString(), errorCode: error.errorCode, message: error.message, statusCode: error.statusCode, field: error.field, details: error.details, userAgent: navigator.userAgent, url: window.location.href, userId: getCurrentUserId(), ...context }; // Log to console in development if (process.env.NODE_ENV === 'development') { console.error('API Error:', errorData); } // Send to error tracking service if (window.errorTracker) { window.errorTracker.captureException(error, { tags: { errorCode: error.errorCode, statusCode: error.statusCode }, extra: errorData }); } // Store in local storage for offline analysis try { const existingErrors = JSON.parse(localStorage.getItem('apiErrors') || '[]'); existingErrors.push(errorData); // Keep only last 50 errors if (existingErrors.length > 50) { existingErrors.splice(0, existingErrors.length - 50); } localStorage.setItem('apiErrors', JSON.stringify(existingErrors)); } catch (e) { console.warn('Failed to store error locally:', e); } }; ``` ### 7. Best Practices Summary 1. **Consistent Error Structure**: Always expect the same error format from your API 2. **User-Friendly Messages**: Map error codes to readable messages 3. **Field-Specific Errors**: Handle validation errors at the field level 4. **Progressive Enhancement**: Start with basic error handling, add sophistication gradually 5. **Logging**: Track errors for debugging and improving user experience 6. **Fallback Handling**: Always have fallback messages for unknown error codes 7. **Loading States**: Show appropriate loading indicators during API calls 8. **Retry Logic**: Implement retry for transient network errors 9. **Offline Handling**: Consider offline scenarios and network recovery This approach gives you robust error handling that scales with your application while providing excellent user experience and debugging capabilities.