This repository has no description
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.