# Error Handling Strategy using Result This document outlines how the `Result` type (`Ok | Err`) from `src/shared/core/Result.ts` should be used for error handling across the different layers of the application, following DDD and layered architecture best practices. ## Core Principle The `Result` type should be used for operations that can **predictably fail** due to invalid input, violated business rules, unavailable resources, or external system issues. It makes potential failures explicit in the function signature and forces the caller to handle both success (`Ok`) and failure (`Err`) paths, improving robustness and clarity. **Avoid using `Result` for:** - Truly exceptional, unrecoverable system errors (e.g., out of memory, critical infrastructure failure). Standard exceptions might be more appropriate here, potentially caught at a high-level boundary. - Flow control that isn't related to a success/failure outcome. ## Usage by Layer ### 1. Domain Layer (`src//domain/`) - **Purpose:** Handles failures related to **business rule validation** and **invariant enforcement**. - **Where:** - **Static factory methods (`create`) for Value Objects and Entities:** These methods are the primary place to enforce invariants. If creation fails due to invalid input or violated rules, return an `Err`. - **Domain Services:** Methods within domain services that perform complex operations or validations involving multiple domain objects might return a `Result`. - **Aggregate Root methods:** Methods that change the state of an aggregate might return a `Result` if the change could violate an invariant based on the provided input or current state. - **Error Type (`E`):** Use specific, fine-grained **domain error classes** derived from a base `DomainError` or similar. Examples: `InvalidEmailError`, `OrderCannotBeCancelledError`, `InsufficientStockError`. These errors convey specific business meaning. - **Example:** ```typescript // src/modules/annotations/domain/value-objects/AnnotationNote.ts import { ValueObject } from '../../../../shared/domain/ValueObject'; import { Result, ok, err } from '../../../../shared/core/Result'; export class InvalidNoteError extends Error { /* ... */ } // Specific Domain Error export class AnnotationNote extends ValueObject<{ value: string }> { public static readonly MAX_LENGTH = 1000; getValue(): string { return this.props.value; } private constructor(props: { value: string }) { super(props); } public static create( value: string, ): Result { if (value.length > this.MAX_LENGTH) { return err( new InvalidNoteError( `Note exceeds maximum length of ${this.MAX_LENGTH}`, ), ); } return ok(new AnnotationNote({ value })); } } ``` ### 2. Application Layer (`src//application/`) - **Purpose:** Handles failures related to **use case orchestration**, **authorization**, finding resources, and mapping errors from lower layers. - **Where:** - **Use Case `execute` methods:** The primary return type for use cases should be `Promise>`. `T` is the successful output (often a DTO or `void`), and `UseCaseError` is a more general application-level error. - **Application Services:** Similar to use cases, services coordinating tasks return `Result`. - **Error Handling:** - Use cases call domain methods and infrastructure repositories/services that return `Result`. - They **must** check the result of each call (`isOk()` / `isErr()`). - If a lower layer returns an `Err`, the use case might: - Return the error directly if it's already a suitable `UseCaseError`. - **Map** the specific domain or infrastructure error to a more general `UseCaseError` (e.g., map `InvalidNoteError` to `ValidationError`, map `DatabaseConnectionError` to `AppError.UnexpectedError`). - Perform compensating actions if necessary. - Use `combine` from `Result.ts` when multiple independent operations must all succeed. - **Error Type (`E`):** Use general **application/use case error classes** derived from `UseCaseError` (`src/shared/core/UseCaseError.ts`). Examples: - `ValidationError`: Input failed validation. - `ResourceNotFoundError`: A required entity (e.g., User, Annotation) wasn't found. - `PermissionDeniedError`: User is not authorized. - `AppError.UnexpectedError`: Wraps unexpected errors caught from lower layers or unforeseen exceptions. **Note:** `UseCaseError` is an abstract class that only provides a `message` property. Derived error classes should not set `this.name` as it's not part of the base class interface. - **Example:** ```typescript // src/modules/annotations/application/use-cases/CreateAnnotationUseCase.ts import { UseCase } from '../../../../shared/core/UseCase'; import { Result, ok, err } from '../../../../shared/core/Result'; import { UseCaseError } from '../../../../shared/core/UseCaseError'; import { AppError } from '../../../../shared/core/AppError'; import { IAnnotationRepository } from '../repositories/IAnnotationRepository'; import { Annotation } from '../../domain/aggregates/Annotation'; import { AnnotationNote, InvalidNoteError, } from '../../domain/value-objects/AnnotationNote'; // ... other imports export class ValidationError extends UseCaseError { constructor(message: string) { super(message); } } export class CreateAnnotationUseCase implements UseCase< InputDTO, Result > { constructor(private repo: IAnnotationRepository) {} async execute( request: InputDTO, ): Promise> { try { const noteResult = AnnotationNote.create(request.note); if (noteResult.isErr()) { // Map DomainError to UseCaseError return err(new ValidationError(noteResult.error.message)); } // ... create other value objects, potentially returning err(...) on failure ... const annotation = Annotation.create({ /* ..., note: noteResult.value, ... */ }); // Assuming Annotation.create doesn't return Result here const saveResult = await this.repo.save(annotation); // Assuming repo.save returns Result if (saveResult.isErr()) { // Wrap InfrastructureError in UnexpectedError return err(AppError.UnexpectedError.create(saveResult.error)); } return ok(undefined); // Success (void) } catch (e) { // Catch any truly unexpected exceptions return err(AppError.UnexpectedError.create(e)); } } } ``` ### 3. Infrastructure Layer (`src//infrastructure/`) - **Purpose:** Handles failures related to **technical concerns** like database access, network communication, external API calls, file system operations, etc. - **Where:** - **Repository implementations:** Methods like `findById`, `save`, `delete` should return `Promise>` or `Promise>`. - **External service clients/adapters:** Adapters interacting with third-party APIs or message queues. - **Publishers:** Implementations of `IPublisher` interfaces. - **Error Handling:** - Catch exceptions thrown by underlying libraries (e.g., database drivers, HTTP clients, SDKs). - Wrap these exceptions into specific `InfrastructureError` types and return them as `Err`. - Do **not** let raw library exceptions leak upwards. - **Error Type (`E`):** Use specific **infrastructure error classes** derived from a base `InfrastructureError` or similar. Examples: `DatabaseError`, `NetworkError`, `ApiUnavailableError`, `FileSystemError`. - **Example:** ```typescript // src/modules/annotations/infrastructure/persistence/repositories/DrizzleAnnotationRepository.ts import { IAnnotationRepository } from '../../../application/repositories/IAnnotationRepository'; import { Result, ok, err } from '../../../../../shared/core/Result'; import { Annotation } from '../../../domain/aggregates/Annotation'; import { TID } from '../../../../../atproto/domain/value-objects/TID'; // ... other imports export class DatabaseError extends Error { constructor(message: string, cause?: Error) { super(message); this.cause = cause; } } // Infrastructure Error export class DrizzleAnnotationRepository implements IAnnotationRepository { // ... constructor ... async findById(id: TID): Promise> { try { const dbResult = await this.db.query.annotationsTable.findFirst({ where: eq(schema.annotationsTable.id, id.toString()), }); if (!dbResult) { return ok(null); // Found nothing, but not an error } const annotation = AnnotationMapper.toDomain(dbResult); // Assuming a mapper return ok(annotation); } catch (e) { console.error('Database error in findById:', e); return err( new DatabaseError( 'Failed to query annotation by ID', e instanceof Error ? e : undefined, ), ); } } async save(annotation: Annotation): Promise> { try { const persistenceModel = AnnotationMapper.toPersistence(annotation); // ... perform drizzle insert/update ... await this.db .insert(schema.annotationsTable) .values(persistenceModel) .onConflictDoUpdate(/* ... */); return ok(undefined); } catch (e) { console.error('Database error saving annotation:', e); return err( new DatabaseError( 'Failed to save annotation', e instanceof Error ? e : undefined, ), ); } } // ... other methods ... } ``` ## Error Handling Flow Summary 1. **Infrastructure:** Catches technical exceptions -> Wraps in `InfrastructureError` -> Returns `Err`. 2. **Application (Use Case):** Calls Infra/Domain -> Checks `Result` -> If `Err`, maps to `UseCaseError` (or `AppError.UnexpectedError`) -> Returns `Err`. 3. **Presentation (e.g., API Controller):** Calls Use Case -> Checks `Result` -> If `Ok`, maps value to success response (e.g., 200 OK) -> If `Err`, maps `UseCaseError` to appropriate error response (e.g., 400 Bad Request, 404 Not Found, 500 Internal Server Error). This layered approach ensures that errors are handled appropriately at each level, preventing low-level details from leaking upwards and providing meaningful error information to the caller at each boundary.