Error Handling System for PRS
This document provides a comprehensive overview of the error handling systems implemented for both PRS Backend and Frontend projects, ensuring consistent error management across the entire application stack.
Overview
The PRS error handling system provides comprehensive error management across both backend and frontend:
Backend Error Handling
- Error Hierarchy: Standardized AppError classes with specific error types
- Error Translation: Automatic translation of database and validation errors
- Error Factory: Convenient factory methods for creating errors
- Global Error Handler: Centralized error processing and logging
- Structured Logging: Integration with PLG stack for error monitoring
Frontend Error Handling
- Error Boundaries: React error boundaries for component error catching
- API Error Handling: Standardized API error formatting and retry logic
- User Notifications: Toast notifications for user-friendly error messages
- Error Components: Fallback UI components for error states
- Correlation IDs: Cross-service error tracking with backend
Error Hierarchy
All errors extend the base AppError class, which provides common functionality:
| Text Only |
|---|
| AppError
├── ValidationError
├── NotFoundError
├── AuthenticationError
├── AuthorizationError
├── BadRequestError
├── ConflictError
└── DatabaseError
|
AppError
Base class for all application errors.
| JavaScript |
|---|
| const { AppError } = require('../app/errors');
throw new AppError(
'Something went wrong',
'CUSTOM_ERROR_CODE',
{ additionalInfo: 'Some details' },
originalError
);
|
ValidationError
Used for validation errors, particularly with Zod schemas.
| JavaScript |
|---|
| const { ValidationError } = require('../app/errors');
throw new ValidationError(
'Validation failed',
{ field1: 'Field is required', field2: 'Must be a number' },
originalError
);
|
NotFoundError
Used when a resource is not found.
| JavaScript |
|---|
| const { NotFoundError } = require('../app/errors');
throw new NotFoundError(
'User',
userId,
'User not found',
originalError
);
|
AuthenticationError
Used for authentication failures.
| JavaScript |
|---|
| const { AuthenticationError } = require('../app/errors');
throw new AuthenticationError(
'Invalid credentials',
'INVALID_PASSWORD',
originalError
);
|
AuthorizationError
Used when a user lacks permission for an action.
| JavaScript |
|---|
| const { AuthorizationError } = require('../app/errors');
throw new AuthorizationError(
'You do not have permission to delete this resource',
'DELETE_RESOURCE',
originalError
);
|
BadRequestError
Used for invalid client requests.
| JavaScript |
|---|
| const { BadRequestError } = require('../app/errors');
throw new BadRequestError(
'Invalid request parameters',
{ details: 'Some details about the error' },
originalError
);
|
ConflictError
Used for resource conflicts, such as duplicate entries.
| JavaScript |
|---|
| const { ConflictError } = require('../app/errors');
throw new ConflictError(
'User with this email already exists',
'User',
'email',
email,
originalError
);
|
DatabaseError
Used for database operation failures.
| JavaScript |
|---|
| const { DatabaseError } = require('../app/errors');
throw new DatabaseError(
'Failed to create user',
'create',
originalError
);
|
Error Factory
The ErrorFactory provides convenient methods for creating errors:
| JavaScript |
|---|
| const { ErrorFactory } = require('../app/errors');
// Create a validation error
throw ErrorFactory.validation('Validation failed', { field: 'Invalid value' });
// Create a not found error
throw ErrorFactory.notFound('User', userId);
// Create an authentication error
throw ErrorFactory.authentication('Invalid credentials');
// Create an authorization error
throw ErrorFactory.authorization('Permission denied', 'DELETE_USER');
// Create a bad request error
throw ErrorFactory.badRequest('Invalid parameters', { details: 'Some details' });
// Create a conflict error
throw ErrorFactory.conflict('User already exists', 'User', 'email', email);
// Create a database error
throw ErrorFactory.database('Database operation failed', 'create');
// Create a generic error
throw ErrorFactory.generic('Something went wrong', 'CUSTOM_ERROR');
|
Error Translation
The ErrorTranslator converts various error types to standardized AppError instances:
| JavaScript |
|---|
| const { ErrorTranslator } = require('../app/errors');
try {
// Some operation that might throw different types of errors
} catch (error) {
// Translate the error to an AppError
const appError = ErrorTranslator.translate(error, {
operation: 'create',
resource: 'User'
});
// Handle the translated error
throw appError;
}
|
Global Error Handler
The global error handler processes all errors and returns standardized responses:
| JavaScript |
|---|
| // This is automatically registered in the application
const { errorHandler } = require('../app/errors');
// All errors thrown in route handlers will be processed by the error handler
fastify.setErrorHandler(errorHandler);
|
All errors are returned in a consistent format:
| JSON |
|---|
| {
"status": 400,
"errorCode": "BAD_REQUEST",
"message": "Invalid request parameters",
"timestamp": "2023-06-01T12:34:56.789Z",
"metadata": {
"details": "Some details about the error"
}
}
|
Migration from Legacy Error System
The legacy error system (clientErrors, serverErrors, etc.) is still available for backward compatibility, but new code should use the new error system.
Legacy:
| JavaScript |
|---|
| const { clientErrors } = require('../app/errors');
throw clientErrors.BAD_REQUEST({
message: 'Invalid request',
description: 'Some details'
});
|
New System:
| JavaScript |
|---|
| const { ErrorFactory } = require('../app/errors');
throw ErrorFactory.badRequest('Invalid request', { details: 'Some details' });
|
PRS Backend Implementation Examples
Requisition Service Error Handling
| JavaScript |
|---|
| // From requisitionController.js - Real PRS implementation
class RequisitionController extends BaseController {
async createRequisition(request, reply) {
const { body, params } = request;
const { userFromToken } = request;
const transaction = await this.sequelize.transaction();
try {
// Create requisition with transaction
const requisition = await this.requisitionRepository.create(body, {
transaction,
});
// Assign approvers
const rsApprovers = await this.requisitionService.rsApproversV2({
category,
projectId,
departmentId,
userFromToken,
transaction,
companyId,
itemList,
});
await this.requisitionService.assignRSApprovers({
rsApprovers,
category,
requisitionId: requisition.id,
transaction,
});
await transaction.commit();
return this.success(reply, requisition);
} catch (error) {
// Log error with structured logging
this.fastify.log.error(
`Requisition create error - ${JSON.stringify(error)}`,
);
await transaction.rollback();
// Use legacy error handling (being migrated to new system)
throw this.clientErrors.BAD_REQUEST({
message: 'Requisition creation failed',
});
}
}
}
|
Authentication Middleware Error Handling
| JavaScript |
|---|
| // From authenticate.js - Real PRS implementation
const authenticate = async (request, reply) => {
const authErrorMsg = 'Authentication failed';
try {
const { id, isForOTP, isForTempPass, isForRefresh } =
await request.jwtVerify();
const isInvalidTokenPayload =
!id || isForOTP || isForTempPass || isForRefresh;
if (isInvalidTokenPayload) {
throw new Error(authErrorMsg);
}
// Get user and validate
const user = await userRepository.findByPk(id);
if (!user) {
throw new Error(authErrorMsg);
}
request.userFromToken = user;
} catch (error) {
// Authentication errors are handled by global error handler
throw error;
}
};
|
Permission Checking with Error Handling
| JavaScript |
|---|
| // From permissionChecker.js - Real PRS implementation
function createPermissionChecker(requiredPermissions, options = {}) {
const { requireAll = false, errorMessage = DEFAULT_PERMISSION_ERROR_MSG } = options;
return async function checkPermissionMiddleware(request, _reply) {
const logger = this.diScope.resolve('logger') || this.log;
const { userFromToken } = request;
try {
if (!hasPermission(userFromToken, requiredPermissions, requireAll)) {
throw ErrorFactory.authorization(errorMessage);
}
} catch (error) {
// Log permission denial with context
logger.warn('Permission denied', {
userId: userFromToken?.id,
username: userFromToken?.username,
requiredPermissions,
requireAll,
});
throw error;
}
};
}
|
Database Error Handling with Logging
| JavaScript |
|---|
| // From databaseLogger.js - Real PRS implementation
function setupDatabaseLogging(sequelize, logger) {
// Log database errors with structured logging
sequelize.addHook('error', (error, options) => {
const { type, model, sql, bind } = options;
const modelName = model ? model.name : 'Unknown';
const operation = determineOperation(sql);
logger.error(`DB ${operation} error on ${modelName}`, {
category: LOG_CATEGORIES.DATABASE,
operation,
model: modelName,
query: sql,
parameters: bind,
error: {
name: error.name,
message: error.message,
stack: error.stack,
},
});
});
}
|
PRS Frontend Implementation Examples
Error Boundary Implementation
| JSX |
|---|
| // From Main.jsx - Real PRS implementation
function MainErrorFallback() {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-background text-foreground">
<div className="max-w-md text-center">
<ErrorIcon className="mx-auto h-16 w-16 fill-red-900" />
<h1 className="mt-6 text-3xl font-bold">Oops! Something went wrong.</h1>
<p className="mt-4 text-muted-foreground">
We apologize for the inconvenience. The page you're looking for
might be temporarily unavailable or there may have been an error in
processing your request.
</p>
<div className="mt-8">
<Button
className="inline-flex items-center"
onClick={() => window.location.assign(window.location.origin)}
>
Return to Home
</Button>
</div>
</div>
</div>
);
}
// From provider.jsx - Global error boundary
<ErrorBoundary FallbackComponent={MainErrorFallback}>
<HelmetProvider>
<QueryClientProvider client={queryClient}>
{children}
<ToastContainer theme="colored" className="w-96" />
</QueryClientProvider>
</HelmetProvider>
</ErrorBoundary>
|
API Error Handling with Retry Logic
| JavaScript |
|---|
| // From apiClient.js - Real PRS implementation
const formatApiError = (error) => {
let errorType = API_ERROR_TYPES.UNKNOWN;
let errorMessage = 'An unexpected error occurred';
let statusCode = null;
let data = null;
// Network errors (no response)
if (error.code === 'ECONNABORTED') {
errorType = API_ERROR_TYPES.TIMEOUT;
errorMessage = ERROR_MESSAGES[errorType];
} else if (error.message === 'Network Error') {
errorType = API_ERROR_TYPES.NETWORK_ERROR;
errorMessage = ERROR_MESSAGES[errorType];
}
// Response errors
else if (error.response) {
statusCode = error.response.status;
data = error.response.data;
// Map status code to error type
errorType = ERROR_STATUS_MAP[statusCode] || API_ERROR_TYPES.UNKNOWN;
// Use server-provided message if available
if (data && data.message) {
errorMessage = data.message;
} else {
errorMessage = ERROR_MESSAGES[errorType];
}
}
return {
type: errorType,
message: errorMessage,
statusCode,
data,
originalError: error,
};
};
// Response interceptor with retry logic
api.interceptors.response.use(
response => response.data,
async error => {
const formattedError = formatApiError(error);
// Handle unauthorized errors
if (formattedError.type === API_ERROR_TYPES.UNAUTHORIZED) {
// Clear auth state
setState({ token: null, type: null, refreshToken: null, expiredAt: null });
setUserState({ user: null, otp: null, secret: null, currentRoute: null });
setPermissionState({ permissions: null });
sessionStorage.removeItem('timeoutState');
}
// Handle network errors with retry logic
else if (
formattedError.type === API_ERROR_TYPES.NETWORK_ERROR ||
formattedError.type === API_ERROR_TYPES.TIMEOUT
) {
const config = error.config;
if (!config.retryCount) {
config.retryCount = 0;
}
if (config.retryCount < MAX_RETRIES) {
config.retryCount += 1;
const delay = getRetryDelay(config.retryCount);
await new Promise(resolve => setTimeout(resolve, delay));
return api(config);
}
}
// Log all errors
console.error('API Error:', {
type: formattedError.type,
message: formattedError.message,
statusCode: formattedError.statusCode,
url: error.config?.url,
method: error.config?.method,
});
error.formattedError = formattedError;
return Promise.reject(error);
},
);
|
Toast Notification Error Handling
| JavaScript |
|---|
| // From useNotification.jsx - Real PRS implementation
const useNotification = () => {
const showNotification = ({ type, message, options = {} }) => {
const toastOptions = { ...defaultOptions, ...options };
const ToastContent = <ToastMessage type={type} message={message} />;
switch (type) {
case 'success':
toast.success(ToastContent, toastOptions);
break;
case 'error':
toast.error(ToastContent, toastOptions);
break;
case 'warning':
toast.warning(ToastContent, toastOptions);
break;
case 'info':
toast.info(ToastContent, toastOptions);
break;
default:
toast(ToastContent, toastOptions);
}
};
return { showNotification };
};
// Usage in components
const { showNotification } = useNotification();
try {
await api.post('/requisitions', data);
showNotification({
type: 'success',
message: 'Requisition created successfully'
});
} catch (error) {
showNotification({
type: 'error',
message: getErrorMessage(error)
});
}
|
Route-Level Error Boundaries
| JSX |
|---|
| // From Root.jsx - Real PRS implementation
const ErrorFallback = ({ error }) => (
<div className="flex flex-col items-center justify-center min-h-[400px]">
<h2 className="text-xl font-semibold mb-4">Something went wrong</h2>
<p className="text-gray-600 mb-4">
{error?.response?.status === 404
? 'The page you are looking for does not exist.'
: 'An unexpected error occurred. Please try again.'}
</p>
<Button onClick={() => window.location.reload()}>
Reload Page
</Button>
</div>
);
export const AppRoot = () => {
const location = useLocation();
return (
<div>
<Suspense fallback={<Spinner size="xl" />}>
<ErrorBoundary
key={location.pathname}
FallbackComponent={ErrorFallback}
onError={error => {
console.error('Error caught by boundary:', error);
}}
>
<Outlet />
</ErrorBoundary>
</Suspense>
</div>
);
};
|
Cross-Service Error Correlation
Backend Error Logging with Correlation IDs
| JavaScript |
|---|
| // Enhanced error handler with correlation IDs
const errorHandler = async function(error, _request, reply) {
const correlationId = _request.headers['x-correlation-id'] || generateCorrelationId();
// Translate error to AppError
const appError = ErrorTranslator.translate(error, {
operation: _request.routeOptions?.config?.url || 'unknown',
resource: _request.routeOptions?.config?.url?.split('/')[1] || 'unknown'
});
// Log error with correlation ID
this.log.error({
correlationId,
errorType: determineErrorType(error),
'x-request-id': _request.id,
message: appError.message,
errorCode: appError.errorCode,
stack: appError.stack,
originalError: appError.originalError?.message,
timestamp: appError.timestamp,
});
// Include correlation ID in response
reply.header('x-correlation-id', correlationId);
return reply.status(appError.getHttpStatus()).send(appError.toResponse());
};
|
Frontend Error Correlation
| JavaScript |
|---|
| // API client with correlation ID propagation
api.interceptors.request.use(config => {
const correlationId = generateCorrelationId();
config.headers['X-Correlation-ID'] = correlationId;
config.headers['X-Request-Source'] = 'prs-frontend';
return config;
});
api.interceptors.response.use(
response => response.data,
error => {
const correlationId = error.config?.headers['X-Correlation-ID'];
console.error('API Error:', {
correlationId,
type: error.formattedError?.type,
message: error.formattedError?.message,
statusCode: error.formattedError?.statusCode,
url: error.config?.url,
method: error.config?.method,
});
return Promise.reject(error);
}
);
|
Best Practices for PRS
Backend Best Practices
- Use ErrorFactory: Prefer factory methods over direct error instantiation
- Transaction Rollback: Always rollback transactions on errors
- Structured Logging: Include context and correlation IDs in error logs
- Error Translation: Use ErrorTranslator for consistent error handling
- Permission Errors: Log permission denials with user context
- Database Errors: Use hooks for automatic database error logging
Frontend Best Practices
- Error Boundaries: Implement at route and component levels
- User-Friendly Messages: Use toast notifications for error feedback
- Retry Logic: Implement automatic retry for network errors
- Correlation IDs: Include correlation IDs for cross-service tracking
- Fallback UI: Provide meaningful fallback components
- Error Logging: Log errors with sufficient context for debugging
Cross-Service Best Practices
- Consistent Error Formats: Align error structures between frontend and backend
- Correlation Tracking: Use correlation IDs for end-to-end error tracking
- Centralized Monitoring: Use PLG stack for unified error monitoring
- Business Context: Include business-relevant context in error logs
- Security Considerations: Sanitize sensitive data in error logs