Overview
The library provides a comprehensive error hierarchy based on CodedError. Each error type has a unique code for programmatic handling and maps to appropriate HTTP status codes.
CodedError Base Class
All errors extend CodedError:
import { CodedError } from '@cosmneo/onion-lasagna/backend/core/global';
abstract class CodedError extends Error {
public readonly code: string;
constructor(options: {
message: string;
code: string;
cause?: unknown;
});
static fromError(cause: unknown): CodedError;
}
Key features:
code- Machine-readable identifier for error handlingcause- ES2022 error chaining supportfromError()- Factory method for wrapping unknown errors
Error Hierarchy
CodedError (base)
├── DomainError
│ ├── InvariantViolationError
│ └── PartialLoadError
├── UseCaseError
│ ├── NotFoundError
│ ├── ConflictError
│ └── UnprocessableError
├── InfraError
│ ├── DbError
│ ├── NetworkError
│ ├── TimeoutError
│ └── ExternalServiceError
├── ControllerError
├── AccessDeniedError
├── InvalidRequestError
└── ObjectValidationError
Error Codes
Each error has a predefined code:
import { ErrorCodes } from '@cosmneo/onion-lasagna/backend/core/global';
// Domain errors
ErrorCodes.Domain.DOMAIN_ERROR // 'DOMAIN_ERROR'
ErrorCodes.Domain.INVARIANT_VIOLATION // 'INVARIANT_VIOLATION'
ErrorCodes.Domain.PARTIAL_LOAD // 'PARTIAL_LOAD'
// Use case errors
ErrorCodes.App.USE_CASE_ERROR // 'USE_CASE_ERROR'
ErrorCodes.App.NOT_FOUND // 'NOT_FOUND'
ErrorCodes.App.CONFLICT // 'CONFLICT'
ErrorCodes.App.UNPROCESSABLE // 'UNPROCESSABLE'
// Infrastructure errors
ErrorCodes.Infra.INFRA_ERROR // 'INFRA_ERROR'
ErrorCodes.Infra.DB_ERROR // 'DB_ERROR'
ErrorCodes.Infra.NETWORK_ERROR // 'NETWORK_ERROR'
ErrorCodes.Infra.TIMEOUT_ERROR // 'TIMEOUT_ERROR'
ErrorCodes.Infra.EXTERNAL_SERVICE_ERROR // 'EXTERNAL_SERVICE_ERROR'
// Presentation errors
ErrorCodes.Presentation.CONTROLLER_ERROR // 'CONTROLLER_ERROR'
ErrorCodes.Presentation.ACCESS_DENIED // 'ACCESS_DENIED'
ErrorCodes.Presentation.INVALID_REQUEST // 'INVALID_REQUEST'
// Global errors
ErrorCodes.Global.OBJECT_VALIDATION_ERROR // 'OBJECT_VALIDATION_ERROR'
Domain Errors
Use for business rule violations in the domain layer:
import { InvariantViolationError } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
class Order {
addItem(item: OrderItem): void {
if (this.status !== 'draft') {
throw new InvariantViolationError({
message: 'Cannot add items to a submitted order',
code: 'ORDER_NOT_DRAFT', // Custom code
});
}
// ...
}
}
PartialLoadError
Thrown when accessing aggregate fields that weren't loaded from the database. This happens when using Load State Tracking:
import { PartialLoadError } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
class OrderAggregate extends BaseAggregateRoot<OrderId, OrderProps> {
get items(): readonly OrderItem[] {
// Throws PartialLoadError if 'items' wasn't marked as loaded
return this.requireLoaded('items');
}
}
// In use case - handle partial load errors
class GetOrderItemsQuery extends BaseInboundAdapter<...> {
protected async handle(input: GetOrderItemsInput): Promise<OrderItemsOutput> {
const order = await this.orderRepo.findById(input.orderId);
if (!order) {
throw new NotFoundError({ message: 'Order not found' });
}
try {
return { items: order.items };
} catch (error) {
if (error instanceof PartialLoadError) {
// Re-fetch with items included
const orderWithItems = await this.orderRepo.findById(
input.orderId,
{ includeItems: true },
);
return { items: orderWithItems!.items };
}
throw error;
}
}
}
Info:
PartialLoadError generates codes like ITEMS_NOT_LOADED by default. Use custom error codes via requireLoaded('items', 'ORDER_ITEMS_MISSING') for more descriptive errors.
Use Case Errors
Use for application-level failures:
import {
NotFoundError,
ConflictError,
UnprocessableError,
} from '@cosmneo/onion-lasagna/backend/core/onion-layers';
class CreateUserCommand {
async execute(input: CreateUserInput): Promise<void> {
const existing = await this.userRepo.findByEmail(input.email);
if (existing) {
throw new ConflictError({
message: `User with email ${input.email} already exists`,
code: 'USER_EMAIL_EXISTS', // Custom code
});
}
// ...
}
}
class GetUserQuery {
async execute(input: GetUserInput): Promise<UserOutput> {
const user = await this.userRepo.findById(input.userId);
if (!user) {
throw new NotFoundError({
message: `User ${input.userId} not found`,
code: 'USER_NOT_FOUND', // Custom code
});
}
return this.toOutput(user);
}
}
Infrastructure Errors
Use for external system failures. BaseOutboundAdapter automatically wraps errors:
import { DbError, NetworkError } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
import { BaseOutboundAdapter } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
class UserRepository extends BaseOutboundAdapter {
constructor(private readonly db: Database) {
super();
}
// Override to customize error type
protected createInfraError(error: unknown, methodName: string): InfraError {
return new DbError({
message: `Database error in ${methodName}`,
cause: error,
});
}
async findById(id: string): Promise<User | null> {
// If this throws, it's automatically wrapped in DbError
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
Presentation Errors
Use for access control and request validation:
import {
AccessDeniedError,
InvalidRequestError,
} from '@cosmneo/onion-lasagna/backend/core/onion-layers';
// Access guard
class CanEditResourceGuard {
async guard(request: Request): Promise<AccessGuardResult> {
const canEdit = await this.checkPermission(request);
if (!canEdit) {
throw new AccessDeniedError({
message: 'You do not have permission to edit this resource',
});
}
return { isAllowed: true };
}
}
Validation Errors
ObjectValidationError includes structured field-level errors:
import { ObjectValidationError } from '@cosmneo/onion-lasagna/backend/core/global';
// Thrown by validators when validation fails
throw new ObjectValidationError({
message: 'Validation failed',
validationErrors: [
{ field: 'email', message: 'Invalid email format' },
{ field: 'password', message: 'Password must be at least 8 characters' },
],
});
Info:
The HTTP response transforms field to item and code to errorCode:
{
"message": "Validation failed",
"errorCode": "VALIDATION_ERROR",
"errorItems": [
{ "item": "email", "message": "Invalid email format" },
{ "item": "password", "message": "Password must be at least 8 characters" }
]
}
HTTP Status Code Mapping
Framework integrations map errors to HTTP status codes:
| Error Type | HTTP Status | Response |
|---|---|---|
ObjectValidationError | 400 Bad Request | Includes validation errors |
InvalidRequestError | 400 Bad Request | Includes validation errors |
AccessDeniedError | 403 Forbidden | Error message |
NotFoundError | 404 Not Found | Error message |
ConflictError | 409 Conflict | Error message |
UnprocessableError | 422 Unprocessable Entity | Error message |
DomainError | 500 Internal Server Error | Masked |
InfraError | 500 Internal Server Error | Masked |
ControllerError | 500 Internal Server Error | Masked |
| Unknown Error | 500 Internal Server Error | Masked |
Internal errors (Domain, Infra, Controller) are masked in responses to prevent leaking implementation details.
Warning:
Masked errors return HTTP 500 with { "message": "An unexpected error occurred", "errorCode": "INTERNAL_ERROR" }. Your custom error message and code are not exposed to clients.
Translating Domain Errors
Since DomainError is masked, you must translate business rule violations to UseCaseError in the use-case layer if you want clients to see them:
import { DomainError, NotFoundError, ConflictError, UnprocessableError } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
class DeleteStatusUseCase extends BaseInboundAdapter<...> {
protected async handle(input: DeleteStatusInputDto): Promise<void> {
const project = await this.projectRepository.findById(input.data.projectId);
if (!project) {
throw new NotFoundError({ message: 'Project not found' });
}
try {
project.deleteStatus(input.data.statusId);
} catch (error) {
// Translate domain errors to use-case errors
if (error instanceof StatusNotFoundError) {
throw new NotFoundError({
message: error.message,
code: 'STATUS_NOT_FOUND'
});
}
if (error instanceof StatusInUseError) {
throw new ConflictError({
message: error.message,
code: 'STATUS_IN_USE'
});
}
if (error instanceof LastStatusError) {
throw new UnprocessableError({
message: error.message,
code: 'LAST_STATUS'
});
}
throw error; // Re-throw unknown errors
}
await this.projectRepository.save(project);
}
}
Info:
When to translate:
NotFoundError(404) - Resource doesn't existConflictError(409) - State conflict preventing actionUnprocessableError(422) - Business rule violation
When to keep masked:
- Invariant violations that indicate bugs
- Internal consistency errors
Error Handling in Route Handlers
The unified route handler automatically handles validation errors. When a route defines schemas, validation failures are automatically converted to ObjectValidationError:
import { serverRoutes } from '@cosmneo/onion-lasagna/http/server';
const routes = serverRoutes(router)
.handle('users.create', {
// If validation fails, ObjectValidationError is thrown automatically
requestMapper: (req) => ({
email: req.body.email,
name: req.body.name,
}),
useCase: createUserUseCase,
responseMapper: (output) => ({
status: 201 as const,
body: { userId: output.userId },
}),
})
.build();
The framework error handlers then convert ObjectValidationError to HTTP 400 responses with structured validation details.
Framework Error Handlers
Each framework integration provides an error handler:
// Hono
import { onionErrorHandler } from '@cosmneo/onion-lasagna/http/frameworks/hono';
app.onError((err, c) => onionErrorHandler(err, c));
// Elysia
import { onionErrorHandler } from '@cosmneo/onion-lasagna/http/frameworks/elysia';
app.onError(({ error }) => onionErrorHandler({ error }));
// Fastify
import { onionErrorHandler } from '@cosmneo/onion-lasagna/http/frameworks/fastify';
app.setErrorHandler(onionErrorHandler);
// NestJS
import { OnionExceptionFilter } from '@cosmneo/onion-lasagna/http/frameworks/nestjs';
@UseFilters(OnionExceptionFilter)
export class AppController {}