Some checks failed
- Add redis-exporter container to docker-compose (oliver006/redis_exporter, 32MB) - Add Redis scrape target to Prometheus config - Add 4 Redis alert rules: RedisDown, HighMemory, HighConnections, RejectedConnections - Document Step 19b (Sentry Error Tracking) in Hetzner deployment guide - Document Step 19c (Redis Monitoring) in Hetzner deployment guide - Update resource budget and port reference tables Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
578 lines
16 KiB
Markdown
578 lines
16 KiB
Markdown
# 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 (
|
|
<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
|
|
```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 (
|
|
<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
|
|
```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 (
|
|
<div className="import-job-status">
|
|
{/* Job status UI */}
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 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.
|