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.
- Features
- Installation
- Quick Start
- Core Concepts
- API Documentation
- Configuration Guide
- SAP CAP Integration
- Error Documentation Endpoints
- Examples
- Best Practices
- Versioning
- License
âś… 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
npm install @douglasragio/cap-error-handlerCreate 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'
]
}
];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');
});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);
}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(withAccept: application/json)
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
}
]
}
}| 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 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-foundINVALID_EMAIL→/errors/invalid-email
Initialize error handler with CAP integration.
Parameters:
cds(Object): SAP CAPcdsinstanceconfig(ErrorHandlerConfig): Configuration objectbaseUrl(string): Public base URL for error documentationerrors(ErrorConfigInput[]): Array of error configurations
Returns: ErrorHandler instance
Example:
const errorHandler = setupErrorHandler(cds, {
baseUrl: 'https://api.example.com/errors',
errors: errorConfig
});Retrieve the initialized ErrorHandler instance.
Parameters:
cds(Object): SAP CAPcdsinstance
Returns: ErrorHandler | null
Example:
const errorHandler = getErrorHandler(cds);
if (errorHandler) {
errorHandler.throw('USER_NOT_FOUND', { userId: '123' });
}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
});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); // 404Check if error is registered.
Parameters:
errorId(string): Error identifier
Returns: boolean
Example:
if (errorHandler.hasError('USER_NOT_FOUND')) {
errorHandler.throw('USER_NOT_FOUND', {});
}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;
}>;
}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}"The error handler automatically registers a documentation endpoint:
GET /errors/{error-id}
Supported Accept headers:
text/html→ Returns HTML documentation pageapplication/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 pageThe error handler registers a CAP error middleware that:
- Catches
StandardErrorinstances thrown by the handler - Converts them to standardized HTTP responses
- Sets appropriate HTTP status codes
- Includes all error details and documentation links
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
- Initialize early - Call
setupErrorHandler()in bootstrap phase - Centralize configuration - Keep all errors in one configuration file
- Use context - Provide relevant context when throwing errors
- Validate early - Configuration is validated during initialization
- Document cause - Always include possible causes for troubleshooting
Request:
GET https://api.example.com/errors/user-not-foundResponse:
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]
Request:
GET https://api.example.com/errors/user-not-found
Accept: application/jsonResponse:
{
"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
}
]
}Request:
GET https://api.example.com/errors/unknown-errorResponse:
Content-Type: text/html; charset=utf-8
Status: 404
[Fiori-styled 404 HTML page indicating error not found]
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": []
}
}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
}
]
}
}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'
]
}
];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_ISSUEWrite 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']
}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'
});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 unavailabilityAssign 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)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
Apache License 2.0
Support & Contributing
For issues, feature requests, or contributions, please refer to the project repository.
For more information: