Skip to content

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

  1. Error Hierarchy: Standardized AppError classes with specific error types
  2. Error Translation: Automatic translation of database and validation errors
  3. Error Factory: Convenient factory methods for creating errors
  4. Global Error Handler: Centralized error processing and logging
  5. Structured Logging: Integration with PLG stack for error monitoring

Frontend Error Handling

  1. Error Boundaries: React error boundaries for component error catching
  2. API Error Handling: Standardized API error formatting and retry logic
  3. User Notifications: Toast notifications for user-friendly error messages
  4. Error Components: Fallback UI components for error states
  5. Correlation IDs: Cross-service error tracking with backend

Error Hierarchy

All errors extend the base AppError class, which provides common functionality:

Text Only
1
2
3
4
5
6
7
8
AppError
├── ValidationError
├── NotFoundError
├── AuthenticationError
├── AuthorizationError
├── BadRequestError
├── ConflictError
└── DatabaseError

AppError

Base class for all application errors.

JavaScript
1
2
3
4
5
6
7
8
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
1
2
3
4
5
6
7
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
1
2
3
4
5
6
7
8
const { NotFoundError } = require('../app/errors');

throw new NotFoundError(
  'User',
  userId,
  'User not found',
  originalError
);

AuthenticationError

Used for authentication failures.

JavaScript
1
2
3
4
5
6
7
const { AuthenticationError } = require('../app/errors');

throw new AuthenticationError(
  'Invalid credentials',
  'INVALID_PASSWORD',
  originalError
);

AuthorizationError

Used when a user lacks permission for an action.

JavaScript
1
2
3
4
5
6
7
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
1
2
3
4
5
6
7
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
1
2
3
4
5
6
7
8
9
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
1
2
3
4
5
6
7
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
1
2
3
4
5
// 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);

Response Format

All errors are returned in a consistent format:

JSON
1
2
3
4
5
6
7
8
9
{
  "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
1
2
3
4
5
6
const { clientErrors } = require('../app/errors');

throw clientErrors.BAD_REQUEST({
  message: 'Invalid request',
  description: 'Some details'
});

New System:

JavaScript
1
2
3
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

  1. Use ErrorFactory: Prefer factory methods over direct error instantiation
  2. Transaction Rollback: Always rollback transactions on errors
  3. Structured Logging: Include context and correlation IDs in error logs
  4. Error Translation: Use ErrorTranslator for consistent error handling
  5. Permission Errors: Log permission denials with user context
  6. Database Errors: Use hooks for automatic database error logging

Frontend Best Practices

  1. Error Boundaries: Implement at route and component levels
  2. User-Friendly Messages: Use toast notifications for error feedback
  3. Retry Logic: Implement automatic retry for network errors
  4. Correlation IDs: Include correlation IDs for cross-service tracking
  5. Fallback UI: Provide meaningful fallback components
  6. Error Logging: Log errors with sufficient context for debugging

Cross-Service Best Practices

  1. Consistent Error Formats: Align error structures between frontend and backend
  2. Correlation Tracking: Use correlation IDs for end-to-end error tracking
  3. Centralized Monitoring: Use PLG stack for unified error monitoring
  4. Business Context: Include business-relevant context in error logs
  5. Security Considerations: Sanitize sensitive data in error logs