This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / docs / error-handling.md
11 kB 226 lines
1# Error Handling Strategy using Result<T, E> 2 3This document outlines how the `Result<T, E>` type (`Ok<T, E> | Err<T, E>`) 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. 4 5## Core Principle 6 7The `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. 8 9**Avoid using `Result` for:** 10 11- 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. 12- Flow control that isn't related to a success/failure outcome. 13 14## Usage by Layer 15 16### 1. Domain Layer (`src/<context>/domain/`) 17 18- **Purpose:** Handles failures related to **business rule validation** and **invariant enforcement**. 19- **Where:** 20 - **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`. 21 - **Domain Services:** Methods within domain services that perform complex operations or validations involving multiple domain objects might return a `Result`. 22 - **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. 23- **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. 24- **Example:** 25 26 ```typescript 27 // src/modules/annotations/domain/value-objects/AnnotationNote.ts 28 import { ValueObject } from '../../../../shared/domain/ValueObject'; 29 import { Result, ok, err } from '../../../../shared/core/Result'; 30 31 export class InvalidNoteError extends Error { 32 /* ... */ 33 } // Specific Domain Error 34 35 export class AnnotationNote extends ValueObject<{ value: string }> { 36 public static readonly MAX_LENGTH = 1000; 37 38 getValue(): string { 39 return this.props.value; 40 } 41 42 private constructor(props: { value: string }) { 43 super(props); 44 } 45 46 public static create( 47 value: string, 48 ): Result<AnnotationNote, InvalidNoteError> { 49 if (value.length > this.MAX_LENGTH) { 50 return err( 51 new InvalidNoteError( 52 `Note exceeds maximum length of ${this.MAX_LENGTH}`, 53 ), 54 ); 55 } 56 return ok(new AnnotationNote({ value })); 57 } 58 } 59 ``` 60 61### 2. Application Layer (`src/<context>/application/`) 62 63- **Purpose:** Handles failures related to **use case orchestration**, **authorization**, finding resources, and mapping errors from lower layers. 64- **Where:** 65 - **Use Case `execute` methods:** The primary return type for use cases should be `Promise<Result<T, UseCaseError>>`. `T` is the successful output (often a DTO or `void`), and `UseCaseError` is a more general application-level error. 66 - **Application Services:** Similar to use cases, services coordinating tasks return `Result`. 67- **Error Handling:** 68 - Use cases call domain methods and infrastructure repositories/services that return `Result`. 69 - They **must** check the result of each call (`isOk()` / `isErr()`). 70 - If a lower layer returns an `Err`, the use case might: 71 - Return the error directly if it's already a suitable `UseCaseError`. 72 - **Map** the specific domain or infrastructure error to a more general `UseCaseError` (e.g., map `InvalidNoteError` to `ValidationError`, map `DatabaseConnectionError` to `AppError.UnexpectedError`). 73 - Perform compensating actions if necessary. 74 - Use `combine` from `Result.ts` when multiple independent operations must all succeed. 75- **Error Type (`E`):** Use general **application/use case error classes** derived from `UseCaseError` (`src/shared/core/UseCaseError.ts`). Examples: 76 - `ValidationError`: Input failed validation. 77 - `ResourceNotFoundError`: A required entity (e.g., User, Annotation) wasn't found. 78 - `PermissionDeniedError`: User is not authorized. 79 - `AppError.UnexpectedError`: Wraps unexpected errors caught from lower layers or unforeseen exceptions. 80 81 **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. 82 83- **Example:** 84 85 ```typescript 86 // src/modules/annotations/application/use-cases/CreateAnnotationUseCase.ts 87 import { UseCase } from '../../../../shared/core/UseCase'; 88 import { Result, ok, err } from '../../../../shared/core/Result'; 89 import { UseCaseError } from '../../../../shared/core/UseCaseError'; 90 import { AppError } from '../../../../shared/core/AppError'; 91 import { IAnnotationRepository } from '../repositories/IAnnotationRepository'; 92 import { Annotation } from '../../domain/aggregates/Annotation'; 93 import { 94 AnnotationNote, 95 InvalidNoteError, 96 } from '../../domain/value-objects/AnnotationNote'; 97 // ... other imports 98 99 export class ValidationError extends UseCaseError { 100 constructor(message: string) { 101 super(message); 102 } 103 } 104 export class CreateAnnotationUseCase 105 implements 106 UseCase< 107 InputDTO, 108 Result<void, ValidationError | AppError.UnexpectedError> 109 > 110 { 111 constructor(private repo: IAnnotationRepository) {} 112 113 async execute( 114 request: InputDTO, 115 ): Promise<Result<void, ValidationError | AppError.UnexpectedError>> { 116 try { 117 const noteResult = AnnotationNote.create(request.note); 118 if (noteResult.isErr()) { 119 // Map DomainError to UseCaseError 120 return err(new ValidationError(noteResult.error.message)); 121 } 122 123 // ... create other value objects, potentially returning err(...) on failure ... 124 125 const annotation = Annotation.create({ 126 /* ..., note: noteResult.value, ... */ 127 }); // Assuming Annotation.create doesn't return Result here 128 129 const saveResult = await this.repo.save(annotation); // Assuming repo.save returns Result<void, InfrastructureError> 130 if (saveResult.isErr()) { 131 // Wrap InfrastructureError in UnexpectedError 132 return err(AppError.UnexpectedError.create(saveResult.error)); 133 } 134 135 return ok(undefined); // Success (void) 136 } catch (e) { 137 // Catch any truly unexpected exceptions 138 return err(AppError.UnexpectedError.create(e)); 139 } 140 } 141 } 142 ``` 143 144### 3. Infrastructure Layer (`src/<context>/infrastructure/`) 145 146- **Purpose:** Handles failures related to **technical concerns** like database access, network communication, external API calls, file system operations, etc. 147- **Where:** 148 - **Repository implementations:** Methods like `findById`, `save`, `delete` should return `Promise<Result<T | null, InfrastructureError>>` or `Promise<Result<void, InfrastructureError>>`. 149 - **External service clients/adapters:** Adapters interacting with third-party APIs or message queues. 150 - **Publishers:** Implementations of `IPublisher` interfaces. 151- **Error Handling:** 152 - Catch exceptions thrown by underlying libraries (e.g., database drivers, HTTP clients, SDKs). 153 - Wrap these exceptions into specific `InfrastructureError` types and return them as `Err`. 154 - Do **not** let raw library exceptions leak upwards. 155- **Error Type (`E`):** Use specific **infrastructure error classes** derived from a base `InfrastructureError` or similar. Examples: `DatabaseError`, `NetworkError`, `ApiUnavailableError`, `FileSystemError`. 156- **Example:** 157 158 ```typescript 159 // src/modules/annotations/infrastructure/persistence/repositories/DrizzleAnnotationRepository.ts 160 import { IAnnotationRepository } from '../../../application/repositories/IAnnotationRepository'; 161 import { Result, ok, err } from '../../../../../shared/core/Result'; 162 import { Annotation } from '../../../domain/aggregates/Annotation'; 163 import { TID } from '../../../../../atproto/domain/value-objects/TID'; 164 // ... other imports 165 166 export class DatabaseError extends Error { 167 constructor(message: string, cause?: Error) { 168 super(message); 169 this.cause = cause; 170 } 171 } // Infrastructure Error 172 173 export class DrizzleAnnotationRepository implements IAnnotationRepository { 174 // ... constructor ... 175 176 async findById(id: TID): Promise<Result<Annotation | null, DatabaseError>> { 177 try { 178 const dbResult = await this.db.query.annotationsTable.findFirst({ 179 where: eq(schema.annotationsTable.id, id.toString()), 180 }); 181 if (!dbResult) { 182 return ok(null); // Found nothing, but not an error 183 } 184 const annotation = AnnotationMapper.toDomain(dbResult); // Assuming a mapper 185 return ok(annotation); 186 } catch (e) { 187 console.error('Database error in findById:', e); 188 return err( 189 new DatabaseError( 190 'Failed to query annotation by ID', 191 e instanceof Error ? e : undefined, 192 ), 193 ); 194 } 195 } 196 197 async save(annotation: Annotation): Promise<Result<void, DatabaseError>> { 198 try { 199 const persistenceModel = AnnotationMapper.toPersistence(annotation); 200 // ... perform drizzle insert/update ... 201 await this.db 202 .insert(schema.annotationsTable) 203 .values(persistenceModel) 204 .onConflictDoUpdate(/* ... */); 205 return ok(undefined); 206 } catch (e) { 207 console.error('Database error saving annotation:', e); 208 return err( 209 new DatabaseError( 210 'Failed to save annotation', 211 e instanceof Error ? e : undefined, 212 ), 213 ); 214 } 215 } 216 // ... other methods ... 217 } 218 ``` 219 220## Error Handling Flow Summary 221 2221. **Infrastructure:** Catches technical exceptions -> Wraps in `InfrastructureError` -> Returns `Err`. 2232. **Application (Use Case):** Calls Infra/Domain -> Checks `Result` -> If `Err`, maps to `UseCaseError` (or `AppError.UnexpectedError`) -> Returns `Err`. 2243. **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). 225 226This 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.