Error Handling
Error Handling
Manacore uses Go-style Result types for explicit error handling, avoiding unexpected exceptions.
Result Type Pattern
Definition
export type Result<T, E = Error> = | { ok: true; value: T } | { ok: false; error: E };
// Helper functionsexport const ok = <T>(value: T): Result<T, never> => ({ ok: true, value,});
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error,});Usage
import { Result, ok, err } from '../types/result';
// Define error typestype UserError = 'NOT_FOUND' | 'INVALID_EMAIL' | 'ALREADY_EXISTS';
// Function with Result return typeasync function findUserByEmail(email: string): Promise<Result<User, UserError>> { if (!isValidEmail(email)) { return err('INVALID_EMAIL'); }
const user = await db.users.findByEmail(email);
if (!user) { return err('NOT_FOUND'); }
return ok(user);}
// Calling the functionconst result = await findUserByEmail('user@example.com');
if (!result.ok) { switch (result.error) { case 'NOT_FOUND': throw new NotFoundException('User not found'); case 'INVALID_EMAIL': throw new BadRequestException('Invalid email format'); case 'ALREADY_EXISTS': throw new ConflictException('User already exists'); }}
// TypeScript knows result.value is User herereturn result.value;Why Result Types?
Problems with Exceptions
// Bad - caller doesn't know what can throwasync function getUser(id: string): Promise<User> { const user = await db.users.find(id); if (!user) throw new Error('Not found'); // Hidden! return user;}
// Caller has no idea this can throwconst user = await getUser(id); // 💥 might explodeBenefits of Results
// Good - explicit about possible failuresasync function getUser(id: string): Promise<Result<User, 'NOT_FOUND'>> { const user = await db.users.find(id); if (!user) return err('NOT_FOUND'); return ok(user);}
// Caller MUST handle the errorconst result = await getUser(id);if (!result.ok) { // Handle error}// TypeScript ensures result.value exists hereError Type Design
Use String Literals
// Good - specific, exhaustivetype CreateUserError = | 'EMAIL_TAKEN' | 'INVALID_EMAIL' | 'WEAK_PASSWORD' | 'RATE_LIMITED';
// Bad - too generictype CreateUserError = Error | string;Include Context When Needed
type ValidationError = { code: 'VALIDATION_FAILED'; field: string; message: string;};
type DatabaseError = { code: 'DATABASE_ERROR'; originalError: Error;};
type CreateUserError = ValidationError | DatabaseError | 'EMAIL_TAKEN';
// Usagefunction validateUser(data: unknown): Result<UserDto, ValidationError> { if (!data.email) { return err({ code: 'VALIDATION_FAILED', field: 'email', message: 'Email is required', }); } // ...}Pattern in Services
Service Layer
@Injectable()export class UsersService { async create(dto: CreateUserDto): Promise<Result<User, CreateUserError>> { // Check for existing user const existing = await this.findByEmail(dto.email); if (existing.ok) { return err('EMAIL_TAKEN'); }
// Validate password if (!this.isStrongPassword(dto.password)) { return err('WEAK_PASSWORD'); }
// Create user try { const user = await this.db.insert(users).values(dto).returning(); return ok(user[0]); } catch (e) { return err('DATABASE_ERROR'); } }
async findById(id: string): Promise<Result<User, 'NOT_FOUND'>> { const [user] = await this.db.select().from(users).where(eq(users.id, id)); return user ? ok(user) : err('NOT_FOUND'); }}Controller Layer
@Controller('api/v1/users')export class UsersController { constructor(private readonly usersService: UsersService) {}
@Post() async create(@Body() dto: CreateUserDto) { const result = await this.usersService.create(dto);
if (!result.ok) { switch (result.error) { case 'EMAIL_TAKEN': throw new ConflictException('Email already registered'); case 'WEAK_PASSWORD': throw new BadRequestException('Password too weak'); case 'DATABASE_ERROR': throw new InternalServerErrorException('Failed to create user'); } }
return result.value; }
@Get(':id') async findOne(@Param('id') id: string) { const result = await this.usersService.findById(id);
if (!result.ok) { throw new NotFoundException('User not found'); }
return result.value; }}Combining Results
Sequential Operations
async function processOrder(orderId: string): Promise<Result<Order, OrderError>> { // Step 1: Find order const orderResult = await findOrder(orderId); if (!orderResult.ok) return orderResult;
// Step 2: Validate const validationResult = validateOrder(orderResult.value); if (!validationResult.ok) return validationResult;
// Step 3: Process payment const paymentResult = await processPayment(orderResult.value); if (!paymentResult.ok) return paymentResult;
// All successful return ok(orderResult.value);}Parallel Operations
async function getUserWithPosts( userId: string): Promise<Result<{ user: User; posts: Post[] }, 'USER_NOT_FOUND' | 'POSTS_FAILED'>> { const [userResult, postsResult] = await Promise.all([ findUser(userId), findPosts(userId), ]);
if (!userResult.ok) return err('USER_NOT_FOUND'); if (!postsResult.ok) return err('POSTS_FAILED');
return ok({ user: userResult.value, posts: postsResult.value, });}When to Throw
// Throw - configuration/programming errorsif (!process.env.DATABASE_URL) { throw new Error('DATABASE_URL must be set');}
// Throw - assertion violations (should never happen)if (user.id !== request.userId) { throw new Error('Invariant violated: user mismatch');}
// Result - expected business casesif (!user) { return err('NOT_FOUND'); // Expected - user might not exist}HTTP Error Mapping
| Result Error | HTTP Status | NestJS Exception |
|---|---|---|
NOT_FOUND | 404 | NotFoundException |
UNAUTHORIZED | 401 | UnauthorizedException |
FORBIDDEN | 403 | ForbiddenException |
VALIDATION_FAILED | 400 | BadRequestException |
CONFLICT / ALREADY_EXISTS | 409 | ConflictException |
RATE_LIMITED | 429 | ThrottlerException |
DATABASE_ERROR | 500 | InternalServerErrorException |
Best Practices
- Be specific - Use descriptive error codes, not generic strings
- Exhaustive handling - Handle all error cases with
switch - Don’t swallow errors - Always handle or propagate
- Document errors - List possible errors in JSDoc
- Keep errors at boundaries - Convert Results to HTTP errors in controllers