Standardized, reusable error handling module for SAP CAP with integrated documentation and SAP Fiori Design Guidelines-compliant HTML rendering
npm install @douglasragio/cap-error-handler-betabash
npm install @douglasragio/cap-error-handler
`
Quick Start
$3
Create an errors.config.ts file with your error definitions:
`typescript
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'
]
}
];
`
$3
In srv/server.ts or equivalent:
`typescript
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
In your service handlers:
`typescript
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);
}
`
$3
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
$3
The error response structure is immutable and follows OData conventions:
`typescript
{
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
}
]
}
}
`
$3
| 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 |
$3
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
$3
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:
`typescript
const errorHandler = setupErrorHandler(cds, {
baseUrl: 'https://api.example.com/errors',
errors: errorConfig
});
`
$3
Retrieve the initialized ErrorHandler instance.
Parameters:
- cds (Object): SAP CAP cds instance
Returns: ErrorHandler | null
Example:
`typescript
const errorHandler = getErrorHandler(cds);
if (errorHandler) {
errorHandler.throw('USER_NOT_FOUND', { userId: '123' });
}
`
$3
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:
`typescript
errorHandler.throw('USER_NOT_FOUND', {
userId: user.ID,
userName: user.name
});
`
$3
Retrieve error configuration by ID.
Parameters:
- errorId (string): Error identifier
Returns: RegisteredError | null
Example:
`typescript
const errorConfig = errorHandler.getError('USER_NOT_FOUND');
console.log(errorConfig?.httpStatus); // 404
`
$3
Check if error is registered.
Parameters:
- errorId (string): Error identifier
Returns: boolean
Example:
`typescript
if (errorHandler.hasError('USER_NOT_FOUND')) {
errorHandler.throw('USER_NOT_FOUND', {});
}
`
Configuration Guide
$3
Each error must define:
`typescript
{
// 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;
}>;
}
`
$3
Use {placeholder} syntax for dynamic values:
`typescript
{
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:
`typescript
errorHandler.throw('USER_NOT_FOUND', { userId: '123' })
// Result: "User 123 not found in {department}"
`
SAP CAP Integration
$3
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:
`bash
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
`
$3
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
$3
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
$3
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
$3
Request:
`bash
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]
`
$3
Request:
`bash
GET https://api.example.com/errors/user-not-found
Accept: application/json
`
Response:
`json
{
"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
}
]
}
`
$3
Request:
`bash
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
$3
`typescript
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:
`json
{
"error": {
"message": "Product P001 not found",
"code": "https://api.example.com/errors/product-not-found",
"target": "Products",
"@Common.numericSeverity": 3,
"details": []
}
}
`
$3
`typescript
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:
`typescript
errorHandler.throw('VALIDATION_FAILED', {
entity: 'Users',
fieldName: 'email'
});
`
Response:
`json
{
"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
}
]
}
}
`
$3
`typescript
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
$3
Follow consistent naming conventions:
`typescript
// 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
`
$3
Write documentation that helps troubleshooting:
`typescript
// 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
Provide relevant context values:
`typescript
// 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'
});
`
$3
Use appropriate HTTP status codes:
`typescript
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
`
$3
Assign severity based on impact:
`typescript
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)
``