Skip to content

douglasragio/cap-error-handler

Repository files navigation

@douglasragio/cap-error-handler

A reusable, standardized error handling module for SAP CAP applications that provides centralized error governance, automatic documentation generation, and SAP Fiori Design Guidelines-compliant HTML rendering.

Table of Contents

Features

âś… Immutable Error Model - Standardized, OData-compliant error response structure
âś… Centralized Error Registry - Define all errors once, reuse across multiple projects
âś… Automatic Documentation - Generate documentation URLs and endpoints automatically
âś… SAP Fiori UI - Responsive, accessible HTML rendering following SAP Fiori Design Guidelines
âś… SAP CAP Integration - Seamless bootstrap and error middleware integration
âś… Dual Response Formats - JSON and HTML responses based on Accept header
âś… Message Interpolation - Dynamic context values in error messages
âś… Stateless & Thread-Safe - Production-ready for high-concurrency environments
âś… Comprehensive Validation - Fail-fast error configuration validation

Installation

npm install @douglasragio/cap-error-handler

Quick Start

1. Define Error Configurations

Create an errors.config.ts file with your error definitions:

import { ErrorConfigInput } from '@douglasragio/cap-error-handler';

export const errorConfig: ErrorConfigInput[] = [
  {
    id: 'USER_NOT_FOUND',
    httpStatus: 404,
    message: 'User {userId} not found in the system',
    target: 'Users',
    severity: 3, // Error level
    doc_title: 'User Not Found',
    doc_description: 'The requested user could not be found in the system.',
    doc_causes: [
      'User ID is incorrect or malformed',
      'User has been deleted',
      'User belongs to a different tenant'
    ],
    doc_solutions: [
      'Verify the user ID before making the request',
      'Check user existence before performing operations',
      'Contact your system administrator for access issues'
    ],
    details: [
      {
        message: 'User ID {userId} was not found in the database',
        target: 'Users.id',
        severity: 2 // Warning level for detail
      }
    ]
  },
  {
    id: 'INVALID_EMAIL',
    httpStatus: 400,
    message: 'Invalid email format: {email}',
    target: 'Users.email',
    severity: 2,
    doc_title: 'Invalid Email Format',
    doc_description: 'The provided email address does not match the required format.',
    doc_causes: [
      'Email is missing @ symbol',
      'Email contains invalid characters',
      'Email exceeds maximum length'
    ],
    doc_solutions: [
      'Provide a valid email address (e.g., user@example.com)',
      'Remove special characters except . - and _',
      'Keep email under 254 characters'
    ]
  }
];

2. Initialize During CAP Bootstrap

In srv/server.ts or equivalent:

import { setupErrorHandler } from '@douglasragio/cap-error-handler';
import { errorConfig } from './errors.config';

cds.on('bootstrap', (cds) => {
  // Initialize error handler with configuration
  setupErrorHandler(cds, {
    baseUrl: 'https://api.example.com/errors', // Public documentation base URL
    errors: errorConfig
  });
  
  console.log('âś“ Error handler initialized');
});

3. Throw Errors from Handlers

In your service handlers:

import { getErrorHandler } from '@douglasragio/cap-error-handler';

class UserService extends cds.ApplicationService {
  async on('READ', 'Users', async (req) => {
    const errorHandler = getErrorHandler(cds);
    const userId = req.data.ID;

    const user = await SELECT.one.from('Users').where({ ID: userId });
    
    if (!user) {
      // Throw standardized error with context
      errorHandler.throw('USER_NOT_FOUND', { userId });
    }

    return user;
  });

  async on('CREATE', 'Users', async (req) => {
    const errorHandler = getErrorHandler(cds);
    const email = req.data.email;

    if (!isValidEmail(email)) {
      errorHandler.throw('INVALID_EMAIL', { email });
    }

    return await INSERT.into('Users').entries(req.data);
  });
}

function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

4. Access Error Documentation

Users can access error documentation at:

  • HTML Documentation: https://api.example.com/errors/user-not-found
  • JSON Documentation: https://api.example.com/errors/user-not-found (with Accept: application/json)

Core Concepts

Error Model Contract

The error response structure is immutable and follows OData conventions:

{
  error: {
    message: "User 123 not found",
    code: "https://api.example.com/errors/user-not-found",
    target: "Users",
    "@Common.numericSeverity": 3,
    details: [
      {
        code: "https://api.example.com/errors/user-not-found",
        message: "User ID 123 was not found in the database",
        target: "Users.id",
        "@Common.numericSeverity": 2
      }
    ]
  }
}

Numeric Severity Levels

Level Label Usage
0 Success Operation completed successfully
1 Info Informational messages
2 Warning Warning conditions
3 Error Error conditions
4 Critical Critical system failures

Error ID Format

Error IDs must follow UPPER_SNAKE_CASE pattern:

  • âś… Valid: USER_NOT_FOUND, INVALID_EMAIL, DB_CONNECTION_FAILED
  • ❌ Invalid: userNotFound, Invalid-Email, db_connection_failed

Error IDs are encoded to URL-safe format for documentation endpoints:

  • USER_NOT_FOUND → /errors/user-not-found
  • INVALID_EMAIL → /errors/invalid-email

API Documentation

setupErrorHandler(cds, config)

Initialize error handler with CAP integration.

Parameters:

  • cds (Object): SAP CAP cds instance
  • config (ErrorHandlerConfig): Configuration object
    • baseUrl (string): Public base URL for error documentation
    • errors (ErrorConfigInput[]): Array of error configurations

Returns: ErrorHandler instance

Example:

const errorHandler = setupErrorHandler(cds, {
  baseUrl: 'https://api.example.com/errors',
  errors: errorConfig
});

getErrorHandler(cds)

Retrieve the initialized ErrorHandler instance.

Parameters:

  • cds (Object): SAP CAP cds instance

Returns: ErrorHandler | null

Example:

const errorHandler = getErrorHandler(cds);
if (errorHandler) {
  errorHandler.throw('USER_NOT_FOUND', { userId: '123' });
}

errorHandler.throw(errorId, context)

Throw a standardized error.

Parameters:

  • errorId (string): Registered error identifier (e.g., 'USER_NOT_FOUND')
  • context (ErrorContext, optional): Object with values for message interpolation

Throws: StandardError with standardized response structure

Example:

errorHandler.throw('USER_NOT_FOUND', { 
  userId: user.ID,
  userName: user.name 
});

errorHandler.getError(errorId)

Retrieve error configuration by ID.

Parameters:

  • errorId (string): Error identifier

Returns: RegisteredError | null

Example:

const errorConfig = errorHandler.getError('USER_NOT_FOUND');
console.log(errorConfig?.httpStatus); // 404

errorHandler.hasError(errorId)

Check if error is registered.

Parameters:

  • errorId (string): Error identifier

Returns: boolean

Example:

if (errorHandler.hasError('USER_NOT_FOUND')) {
  errorHandler.throw('USER_NOT_FOUND', {});
}

Configuration Guide

ErrorConfigInput Structure

Each error must define:

{
  // Unique identifier (UPPER_SNAKE_CASE)
  id: string;
  
  // HTTP status code
  httpStatus: number; // 100-599
  
  // Message template with optional {placeholder} syntax
  message: string;
  
  // Functional target (e.g., entity name, field name)
  target: string;
  
  // Numeric severity (0-4)
  severity: NumericSeverity;
  
  // Documentation: Title
  doc_title: string;
  
  // Documentation: Full description
  doc_description: string;
  
  // Documentation: List of causes
  doc_causes: string[];
  
  // Documentation: List of solutions
  doc_solutions: string[];
  
  // Optional: Detail items for specific error aspects
  details?: Array<{
    message: string;      // Detail message template
    target: string;       // Specific target for this detail
    severity: NumericSeverity;
  }>;
}

Message Interpolation

Use {placeholder} syntax for dynamic values:

{
  id: 'USER_NOT_FOUND',
  message: 'User {userId} not found in {department}',
  // When thrown with context:
  // errorHandler.throw('USER_NOT_FOUND', { userId: '123', department: 'Sales' })
  // Result: "User 123 not found in Sales"
}

Unreplaced placeholders are left as-is:

errorHandler.throw('USER_NOT_FOUND', { userId: '123' })
// Result: "User 123 not found in {department}"

SAP CAP Integration

Automatic Endpoint Registration

The error handler automatically registers a documentation endpoint:

GET /errors/{error-id}

Supported Accept headers:

  • text/html → Returns HTML documentation page
  • application/json → Returns JSON documentation
  • Default (no header) → Returns HTML documentation

Example requests:

# Get HTML documentation
curl https://api.example.com/errors/user-not-found

# Get JSON documentation
curl -H "Accept: application/json" https://api.example.com/errors/user-not-found

# Unknown error
curl https://api.example.com/errors/unknown-error
# Returns 404 HTML page

Error Middleware

The error handler registers a CAP error middleware that:

  1. Catches StandardError instances thrown by the handler
  2. Converts them to standardized HTTP responses
  3. Sets appropriate HTTP status codes
  4. Includes all error details and documentation links

Integration Points

The module integrates with:

  • Bootstrap: Initializes during CAP startup
  • Routing: Registers documentation endpoints
  • Error Handling: Middleware for StandardError conversion
  • Context: Stores handler in Symbol for retrieval

Best Practices

  1. Initialize early - Call setupErrorHandler() in bootstrap phase
  2. Centralize configuration - Keep all errors in one configuration file
  3. Use context - Provide relevant context when throwing errors
  4. Validate early - Configuration is validated during initialization
  5. Document cause - Always include possible causes for troubleshooting

Error Documentation Endpoints

HTML Response (Default)

Request:

GET https://api.example.com/errors/user-not-found

Response:

Content-Type: text/html; charset=utf-8
Status: 200

[SAP Fiori-styled HTML page with:
- Error title and severity indicator
- Full description
- Possible causes (bulleted list)
- Recommended solutions (bulleted list)
- Technical information (status code, target, severity)
- Error details if applicable]

JSON Response

Request:

GET https://api.example.com/errors/user-not-found
Accept: application/json

Response:

{
  "id": "USER_NOT_FOUND",
  "title": "User Not Found",
  "description": "The requested user could not be found in the system.",
  "causes": [
    "User ID is incorrect",
    "User has been deleted",
    "User belongs to another tenant"
  ],
  "solutions": [
    "Verify the user ID",
    "Check user existence",
    "Contact administrator"
  ],
  "technical": {
    "httpStatus": 404,
    "target": "Users",
    "severity": 3,
    "severityLabel": "Error",
    "code": "https://api.example.com/errors/user-not-found"
  },
  "details": [
    {
      "message": "User ID {userId} was not found in the database",
      "target": "Users.id",
      "severity": 2
    }
  ]
}

Not Found Response

Request:

GET https://api.example.com/errors/unknown-error

Response:

Content-Type: text/html; charset=utf-8
Status: 404

[Fiori-styled 404 HTML page indicating error not found]

Examples

Example 1: Basic Error Throwing

class ProductService extends cds.ApplicationService {
  async on('READ', 'Products', async (req) => {
    const errorHandler = getErrorHandler(cds);
    const productId = req.data.ID;

    const product = await SELECT.one.from('Products').where({ ID: productId });
    
    if (!product) {
      errorHandler.throw('PRODUCT_NOT_FOUND', { productId });
    }

    return product;
  });
}

Error Response:

{
  "error": {
    "message": "Product P001 not found",
    "code": "https://api.example.com/errors/product-not-found",
    "target": "Products",
    "@Common.numericSeverity": 3,
    "details": []
  }
}

Example 2: Error with Details

const errorConfig: ErrorConfigInput[] = [
  {
    id: 'VALIDATION_FAILED',
    httpStatus: 400,
    message: 'Validation failed for entity {entity}',
    target: 'Validation',
    severity: 2,
    doc_title: 'Validation Failed',
    doc_description: 'One or more validation rules were violated.',
    doc_causes: ['Required field is empty', 'Field format is invalid'],
    doc_solutions: ['Check field values', 'Review validation rules'],
    details: [
      {
        message: 'Required field {fieldName} is empty',
        target: 'Validation.{fieldName}',
        severity: 3
      },
      {
        message: 'Field {fieldName} exceeds maximum length',
        target: 'Validation.{fieldName}',
        severity: 2
      }
    ]
  }
];

Error thrown:

errorHandler.throw('VALIDATION_FAILED', { 
  entity: 'Users',
  fieldName: 'email'
});

Response:

{
  "error": {
    "message": "Validation failed for entity Users",
    "code": "https://api.example.com/errors/validation-failed",
    "target": "Validation",
    "@Common.numericSeverity": 2,
    "details": [
      {
        "code": "https://api.example.com/errors/validation-failed",
        "message": "Required field email is empty",
        "target": "Validation.email",
        "@Common.numericSeverity": 3
      },
      {
        "code": "https://api.example.com/errors/validation-failed",
        "message": "Field email exceeds maximum length",
        "target": "Validation.email",
        "@Common.numericSeverity": 2
      }
    ]
  }
}

Example 3: Complex Configuration

export const errorConfig: ErrorConfigInput[] = [
  {
    id: 'DATABASE_UNAVAILABLE',
    httpStatus: 503,
    message: 'Database service is currently unavailable',
    target: 'Database',
    severity: 4, // Critical
    doc_title: 'Database Unavailable',
    doc_description: 'The database service is temporarily unavailable. Please retry your request.',
    doc_causes: [
      'Database server is down for maintenance',
      'Network connectivity issue',
      'Database connection pool exhausted',
      'Database authentication failed'
    ],
    doc_solutions: [
      'Wait for maintenance window to complete',
      'Check network connectivity',
      'Reduce concurrent connections',
      'Verify database credentials',
      'Contact database administrator'
    ]
  }
];

Best Practices

1. Error ID Naming

Follow consistent naming conventions:

// Good: Clear, descriptive, grouped by domain
USER_NOT_FOUND
USER_ALREADY_EXISTS
PERMISSION_DENIED
DATABASE_CONNECTION_FAILED
INVALID_REQUEST_FORMAT

// Avoid: Vague, technical jargon
ERROR_1
BAD_DATA
FAIL
UNKNOWN_ISSUE

2. Documentation Quality

Write documentation that helps troubleshooting:

// Good: Actionable, specific
{
  doc_description: 'The user account specified in the request does not exist in the system.',
  doc_causes: [
    'User ID was typed incorrectly',
    'User account was deleted within the last 30 days',
    'User belongs to a different tenant in multi-tenant setup'
  ],
  doc_solutions: [
    'Verify the user ID matches the intended user',
    'Check user list to confirm account exists',
    'Ensure you are accessing the correct tenant'
  ]
}

// Avoid: Vague, unhelpful
{
  doc_description: 'User not found.',
  doc_causes: ['User does not exist'],
  doc_solutions: ['Try again']
}

3. Context Interpolation

Provide relevant context values:

// Good: Rich context for troubleshooting
errorHandler.throw('PAYMENT_FAILED', {
  orderId: order.ID,
  amount: order.totalAmount,
  paymentMethod: order.paymentMethod,
  errorCode: paymentGateway.errorCode
});

// Avoid: Insufficient context
errorHandler.throw('PAYMENT_FAILED', {
  error: 'failed'
});

4. HTTP Status Codes

Use appropriate HTTP status codes:

400 // Bad Request - Client error (invalid input)
401 // Unauthorized - Missing authentication
403 // Forbidden - Insufficient permissions
404 // Not Found - Resource doesn't exist
409 // Conflict - State conflict (already exists)
500 // Internal Server Error - Server error
503 // Service Unavailable - Temporary unavailability

5. Severity Assignment

Assign severity based on impact:

0 // Success - Operation completed
1 // Info - Informational message
2 // Warning - Non-critical issue (proceed with caution)
3 // Error - Operation failed (must be resolved)
4 // Critical - System failure (requires immediate action)

Versioning

This module follows Semantic Versioning (semver):

  • MAJOR: Breaking changes to error model or HTML rendering
  • MINOR: New features (new errors, new fields that don't break existing contracts)
  • PATCH: Bug fixes, internal improvements

Breaking changes include:

  • Adding/removing/renaming fields in the error model
  • Changing field types in the error model
  • Changing HTML structure or styling significantly
  • Changing endpoint paths

Non-breaking changes include:

  • Adding new error definitions
  • Enhancing HTML styles without breaking layout
  • Improving documentation generation

License

Apache License 2.0


Support & Contributing

For issues, feature requests, or contributions, please refer to the project repository.

For more information:

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published