@beethovn/errors
v1.0.0
Published
Standardized error types for the Beethovn platform
Maintainers
Readme
@beethovn/errors
Standardized error types for the Beethovn platform. Provides structured, typed errors with rich metadata for better debugging, logging, and monitoring.
Installation
pnpm add @beethovn/errorsQuick Start
import { ValidationError, ErrorFactory, isDomainError } from '@beethovn/errors';
// Throw typed errors
if (!email.includes('@')) {
throw new ValidationError('Invalid email format', 'email');
}
// Handle unknown errors
try {
await riskyOperation();
} catch (error: unknown) {
const beethovnError = ErrorFactory.fromUnknown(error, 'riskyOperation');
logger.error('Operation failed', beethovnError.toJSON());
if (isDomainError(error)) {
// User-fixable error
return { error: beethovnError.toClient() };
}
}Error Hierarchy
BeethovnError (base)
├── DomainError (business logic)
│ ├── ValidationError
│ ├── BusinessRuleError
│ ├── NotFoundError
│ ├── ConflictError
│ └── InvalidStateError
├── InfrastructureError (technical)
│ ├── DatabaseError
│ ├── NetworkError
│ ├── ExternalServiceError
│ ├── TimeoutError
│ ├── RateLimitError
│ └── UnknownError
├── AuthError (authentication/authorization)
│ ├── InvalidCredentialsError
│ ├── TokenExpiredError
│ ├── InvalidTokenError
│ ├── PermissionDeniedError
│ ├── ForbiddenError
│ └── UnauthenticatedError
└── IntegrationError (inter-service communication)
├── EventPublishError
├── MessageQueueError
├── WebSocketError
└── EventSubscriptionErrorUsage Examples
Domain Errors
import {
ValidationError,
BusinessRuleError,
NotFoundError,
ConflictError,
} from '@beethovn/errors';
// Validation error
if (!email.includes('@')) {
throw new ValidationError('Invalid email format', 'email', {
value: email,
});
}
// Business rule violation
if (balance < amount) {
throw new BusinessRuleError(
'Insufficient balance',
'MINIMUM_BALANCE',
{ balance, required: amount }
);
}
// Not found
const user = await db.getUser(id);
if (!user) {
throw new NotFoundError('User', id, { tenantId });
}
// Conflict
if (existingUser) {
throw new ConflictError(
'User with this email already exists',
{ email }
);
}Infrastructure Errors
import {
DatabaseError,
NetworkError,
ExternalServiceError,
TimeoutError,
} from '@beethovn/errors';
// Database error
try {
await db.query(sql, params);
} catch (error: unknown) {
throw new DatabaseError('saveUser', error, { userId });
}
// Network error
try {
const response = await fetch(url);
} catch (error: unknown) {
throw new NetworkError('Failed to fetch user data', { url, method: 'GET' });
}
// External service error
try {
await stripeClient.charge(amount);
} catch (error: unknown) {
throw new ExternalServiceError(
'Stripe',
'createCharge',
error,
{ amount, currency }
);
}
// Timeout error
throw new TimeoutError('Database query', 5000, { query: sql });Auth Errors
import {
InvalidCredentialsError,
TokenExpiredError,
PermissionDeniedError,
} from '@beethovn/errors';
// Invalid credentials
const user = await verifyCredentials(email, password);
if (!user) {
throw new InvalidCredentialsError({ email });
}
// Token expired
if (token.exp < Date.now()) {
throw new TokenExpiredError({ userId: token.sub });
}
// Permission denied
if (!user.permissions.includes('delete:product')) {
throw new PermissionDeniedError('Product', 'delete', {
userId: user.id,
requiredPermission: 'delete:product',
});
}Error Factory
import { ErrorFactory } from '@beethovn/errors';
// Convert unknown errors
try {
await operation();
} catch (error: unknown) {
throw ErrorFactory.fromUnknown(error, 'operationContext');
}
// Create database error
throw ErrorFactory.database('saveUser', error, { userId });
// Create validation error
throw ErrorFactory.validation('Invalid email', 'email');
// Aggregate multiple errors
const errors = [
new ValidationError('Invalid email', 'email'),
new ValidationError('Invalid age', 'age'),
];
throw ErrorFactory.aggregate(errors, 'User validation');Type Guards
import {
isBeethovnError,
isDomainError,
isInfrastructureError,
isRecoverableError,
isClientSafeError,
} from '@beethovn/errors';
try {
await operation();
} catch (error: unknown) {
if (isBeethovnError(error)) {
logger.error('Beethovn error', error.toJSON());
}
if (isDomainError(error)) {
// User-fixable error - return helpful message
return { error: error.toClient() };
}
if (isInfrastructureError(error) && error.recoverable) {
// Retry recoverable infrastructure errors
await retryWithBackoff(operation);
}
if (isClientSafeError(error)) {
// Safe to send to client
res.status(error.statusCode).json(error.toClient());
} else {
// Internal error - don't expose details
res.status(500).json({ error: 'Internal server error' });
}
}Error Properties
All errors extend BeethovnError and include:
code- Unique error code (e.g., 'VALIDATION_ERROR')statusCode- HTTP status code (e.g., 400, 404, 500)message- Human-readable error messagemetadata- Additional context for debuggingtimestamp- When the error occurredrecoverable- Whether the error can be retriedstack- Stack trace for debugging
Error Methods
toJSON()
Serialize error for logging and monitoring (includes all details):
const error = new ValidationError('Invalid email', 'email');
logger.error('Validation failed', error.toJSON());
// {
// name: 'ValidationError',
// code: 'VALIDATION_ERROR',
// message: 'Invalid email',
// statusCode: 400,
// metadata: { field: 'email' },
// timestamp: '2025-01-03T10:00:00.000Z',
// recoverable: false,
// stack: '...'
// }toClient()
Sanitized error for API responses (excludes stack trace and sensitive data):
const error = new ValidationError('Invalid email', 'email');
res.status(error.statusCode).json(error.toClient());
// {
// code: 'VALIDATION_ERROR',
// message: 'Invalid email',
// statusCode: 400,
// timestamp: '2025-01-03T10:00:00.000Z'
// }Integration with DbResult
import { DbResult } from '@beethovn/types';
import { NotFoundError, ErrorFactory } from '@beethovn/errors';
async function getUser(id: string): Promise<DbResult<User>> {
try {
const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
if (!user) {
return {
data: null,
error: new NotFoundError('User', id),
};
}
return { data: user, error: null };
} catch (error: unknown) {
return {
data: null,
error: ErrorFactory.database('getUser', error, { userId: id }),
};
}
}Best Practices
- Always use typed errors - Never throw generic
Error - Include context - Add relevant metadata for debugging
- Use ErrorFactory.fromUnknown - For handling unknown errors
- Check recoverability - Retry recoverable errors
- Sanitize client responses - Use
toClient()for API responses - Log with toJSON() - Include full error details in logs
- Use type guards - Handle different error types appropriately
Error Decision Tree
Is this a business logic issue?
├─ YES → Use DomainError
│ ├─ Invalid input? → ValidationError
│ ├─ Rule violation? → BusinessRuleError
│ ├─ Not found? → NotFoundError
│ └─ Already exists? → ConflictError
│
└─ NO → Is it a technical issue?
├─ YES → Use InfrastructureError
│ ├─ Database? → DatabaseError
│ ├─ Network? → NetworkError
│ └─ External service? → ExternalServiceError
│
└─ NO → Is it auth/authz?
├─ YES → Use AuthError
└─ NO → Use ErrorFactory.fromUnknown()TypeScript Support
Full TypeScript support with type inference:
import { BeethovnError, ValidationError } from '@beethovn/errors';
function handleError(error: BeethovnError) {
// TypeScript knows all properties
console.log(error.code); // ✓ string
console.log(error.statusCode); // ✓ number
console.log(error.metadata); // ✓ Record<string, unknown>
}
// Type narrowing with type guards
if (error instanceof ValidationError) {
// TypeScript knows this is ValidationError
console.log(error.metadata.field); // ✓ unknown
}License
MIT
Contributing
See CONTRIBUTING.md
