16 KiB
16 KiB
Frontend Exception Handling Guide
Understanding Your API's Exception Structure
Your API returns consistent error responses with this structure:
{
"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
// 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
// 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:
// 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.',
// Stock errors
INSUFFICIENT_STOCK: 'Not enough stock available for this operation.',
STOCK_NOT_FOUND: 'No stock information found for this product.',
NEGATIVE_STOCK_NOT_ALLOWED: 'Stock 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
// 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
// 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 (
<form onSubmit={handleSubmit}>
{error && !error.field && (
<div className="error-banner">
{error.message}
</div>
)}
<div className="form-field">
<label>MarketplaceProduct ID</label>
<input
type="text"
value={formData.product_id}
onChange={(e) => setFormData({...formData, product_id: e.target.value})}
className={fieldErrors.product_id ? 'error' : ''}
/>
{fieldErrors.product_id && (
<span className="field-error">{fieldErrors.product_id}</span>
)}
</div>
<div className="form-field">
<label>MarketplaceProduct Name</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
className={fieldErrors.name ? 'error' : ''}
/>
{fieldErrors.name && (
<span className="field-error">{fieldErrors.name}</span>
)}
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Creating...' : 'Create MarketplaceProduct'}
</button>
</form>
);
};
4. Global Error Handler
React Error Boundary
// 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 (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>We're sorry, but something unexpected happened.</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
Global Toast Notifications
// 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
// 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;
}
);
Stock Management Errors
// components/StockManager.jsx
const handleStockUpdate = async (gtin, location, quantity) => {
try {
await updateStock(gtin, location, quantity);
notificationManager.notify('success', 'Stock updated successfully');
} catch (error) {
switch (error.errorCode) {
case 'INSUFFICIENT_STOCK':
const { available_quantity, requested_quantity } = error.details;
notificationManager.notify('error',
`Cannot remove ${requested_quantity} items. Only ${available_quantity} available.`
);
break;
case 'STOCK_NOT_FOUND':
notificationManager.notify('error', 'No stock record found for this product');
break;
case 'NEGATIVE_STOCK_NOT_ALLOWED':
notificationManager.notify('error', 'Stock quantity cannot be negative');
break;
default:
notificationManager.notifyError(error);
}
}
};
Import Job Monitoring
// 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 (
<div className="import-job-status">
{/* Job status UI */}
</div>
);
};
6. Error Logging and Monitoring
// 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
- Consistent Error Structure: Always expect the same error format from your API
- User-Friendly Messages: Map error codes to readable messages
- Field-Specific Errors: Handle validation errors at the field level
- Progressive Enhancement: Start with basic error handling, add sophistication gradually
- Logging: Track errors for debugging and improving user experience
- Fallback Handling: Always have fallback messages for unknown error codes
- Loading States: Show appropriate loading indicators during API calls
- Retry Logic: Implement retry for transient network errors
- 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.