Instruction file imported from Neurotypic-ai/magus-mark (
.cursor/rules/error-handling.mdc). Copyright stays with the author.
description: Error handling patterns and best practices for Obsidian Magic globs: /*.ts,/*.tsx alwaysApply: false
Error Handling System
Core Principles
When implementing error handling in this project, follow these core principles:
- Type-safe error hierarchy - Use specialized error classes for different failure scenarios
- Result pattern - Wrap operations that can fail in Result<T, E> objects
- Retry mechanisms - Apply retries with exponential backoff for transient failures
- Utility functions - Use provided helpers for error transformation and recovery
All error handling is centralized in packages/core/src/errors.ts. Never create custom error handling logic outside this system.
Error Class Hierarchy
AppError- Base class with code, cause, context, and recoverable propertiesValidationError- For input validation failuresFileSystemError- For file system operation failuresNetworkError- For network communication issuesAPIError- For API-specific failures with status codes and rate limitsApiKeyError- For authentication issuesConfigurationError- For system configuration problemsMarkdownError- For markdown processing failuresTaggingError- For tagging operation failuresCostLimitError- For budget/token limit exceedances
Result Pattern Usage
Always return Result<T, E> from functions that can fail rather than throwing exceptions. This enables:
- Type-safe error handling
- Method chaining with
andThen()andmap() - Clear distinction between success and failure paths
Key methods:
Result.ok<T>(value)- Create a successful resultResult.fail<T, E>(error)- Create a failed resultresult.isOk()/result.isFail()- Check result statusresult.getValue()/result.getError()- Access result contentsresult.andThen(fn)- Chain operations returning Results
Coding Patterns
-
Use specialized error types with context:
throw new FileSystemError('File not found', { path, code: ErrorCodes.FILE_NOT_FOUND }); -
Return Results from operations that can fail:
function processMightFail(): Result<Value> { try { return Result.ok(value); } catch (err) { return Result.fail(toAppError(err)); } } -
Chain operations with Results:
return fetchData() .andThen(data => processData(data)) .andThen(processed => saveResult(processed)); -
Use retry for transient failures:
const data = await withRetry(() => fetchDataFromApi()); -
Always clean up resources:
try { resource = await acquireResource(); return Result.ok(await processResource(resource)); } finally { if (resource) await releaseResource(resource).catch(console.error); } -
Validate inputs early:
if (!isValidInput(input)) { return Result.fail(new ValidationError('Invalid input')); } -
Log errors with context:
logger.error({ message: 'Operation failed', error: error instanceof AppError ? error.format() : String(error), context: { operationId, user, timestamp } });
Implementation Reference
For implementation details, refer to:
Error Handling
The project uses a consistent error handling pattern based on the Result type:
Result Pattern
The core of our error handling is the Result<T, E = AppError> type:
// Type definition (simplified)
export type Result<T, E = AppError> =
| { isOk: true; isErr: false; value: T }
| { isOk: false; isErr: true; error: E };
Creating Results
// Success case
const success = Result.ok(value);
// Error case
const failure = Result.fail(new ValidationError('Invalid input'));
// From nullable
const fromValue = Result.fromNullable(maybeValue, () => new NotFoundError('Value not found'));
// From promise/try-catch
const fromPromise = await Result.fromPromise(
asyncOperation(),
(err) => toAppError(err)
);
Working with Results
// Checking result status
if (result.isOk()) {
// TypeScript knows result.value is available here
const value = result.value;
} else {
// TypeScript knows result.error is available here
const error = result.error;
}
// Result chaining
const finalResult = result
.andThen(value => processStep1(value))
.andThen(intermediateResult => processStep2(intermediateResult))
.orElse(error => {
if (error instanceof ResourceNotFoundError) {
return createResource().andThen(newResource => processStep1(newResource));
}
return Result.fail(error);
});
// Result mapping
const mappedResult = result
.map(value => transformValue(value))
.mapErr(error => new UserFriendlyError('Operation failed', { cause: error }));
// Unwrapping (use sparingly, prefer pattern matching)
try {
const value = result.unwrap(); // Throws if result is an error
} catch (err) {
// Handle error
}
Error Types
We use a hierarchical error system:
AppError (base error type)
├── ValidationError
│ └── SchemaValidationError
├── NotFoundError
│ ├── FileNotFoundError
│ └── ResourceNotFoundError
├── PermissionError
├── NetworkError
│ ├── ApiError
│ │ └── OpenAIError
│ └── TimeoutError
├── ConfigurationError
├── OperationCancelledError
└── InternalError
└── UnreachableError
Creating Custom Errors
// Extending the base error class
export class CustomError extends AppError {
constructor(message: string, options?: ErrorOptions) {
super(message, { ...options, code: 'CUSTOM_ERROR' });
}
}
// Using in context
function processData(input: unknown): Result<ProcessedData> {
if (!isValidInput(input)) {
return Result.fail(new ValidationError('Invalid input format'));
}
try {
// Processing logic
return Result.ok(processed);
} catch (err) {
return Result.fail(toAppError(err));
}
}
Error Conversion
We use utilities to convert unknown errors:
// Convert any error to a typed AppError
const appError = toAppError(unknownError);
// Convert with context
const contextualError = toAppError(err, {
defaultMessage: 'Failed to process file',
context: { fileName }
});
Async Error Handling
For asynchronous operations, we combine async/await with Result:
async function processFile(path: string): Promise<Result<ProcessedFile>> {
// Read file with Result handling
const fileResult = await Result.fromPromise(
fs.readFile(path, 'utf-8'),
err => toAppError(err, { context: { path } })
);
// Early return on error
if (fileResult.isErr()) {
return fileResult;
}
// Chain processing
return parseFile(fileResult.value)
.andThen(parsed => validateFile(parsed))
.andThen(validated => transformFile(validated));
}
Testing Errors
Testing code that uses Result pattern:
describe('processFile', () => {
it('should return an error for invalid file', async () => {
const result = await processFile('nonexistent.txt');
expect(result.isErr()).toBe(true);
expect(result.error).toBeInstanceOf(FileNotFoundError);
});
it('should process valid file successfully', async () => {
const result = await processFile('valid.txt');
expect(result.isOk()).toBe(true);
expect(result.value).toEqual(expect.objectContaining({
id: expect.any(String)
}));
});
});
Best Practices
- Be Explicit: Use specific error types that clearly indicate what went wrong
- Provide Context: Include relevant data in errors (file paths, IDs, etc.)
- Handle Early: Check for errors at the boundary of operations
- Chain Operations: Use
andThento compose operations that return Results - Avoid Exceptions: Prefer returning Result over throwing exceptions
- Convert External Errors: Always convert external library errors to our types
- Document Error Paths: Document possible error types in function JSDoc
- Test Error Cases: Always test both success and error paths