Error Handling
Building a consistent error handling strategy is key for scalable, maintainable systems.
DomusJS provides two main tools:
✅ The Result Pattern → for domain and application-level flow control.
✅ The BaseError Hierarchy → for structured, throw-able errors (especially for HTTP layers).
🧩 When to Use Each?
Section titled “🧩 When to Use Each?”Layer | What to Use |
---|---|
Domain Layer | Result.ok , Result.fail or domain-specific errors (no throw ) |
Command / Query Handlers | Return Result (no exceptions) |
Controller / API Layer | Convert Result.fail to BaseError or HTTP response |
Infrastructure Failures | Throw structured BaseError (e.g., NotFoundError ) or let bubble up to global handler |
✅ Rule of Thumb: Keep the domain pure and predictable; handle infrastructure and HTTP concerns at the edges.
Result Pattern Recap
Section titled “Result Pattern Recap”The Result
object allows:
- Explicitly signaling success or failure.
- Returning meaningful error messages or data.
- Avoiding exceptions in normal control flows.
Example:
const result = await service.doSomething();
if (result.isFailure()) { return Result.fail('Business rule broken');}
return Result.ok(data);
✅ Use this pattern inside handlers and services.
BaseError Recap
Section titled “BaseError Recap”DomusJS provides a BaseError
class:
export abstract class BaseError extends Error { public readonly name: string; public readonly code: string; public readonly statusCode: number;
constructor(message: string, code: string, statusCode: number) { super(message); this.name = this.constructor.name; this.code = code; this.statusCode = statusCode; Error.captureStackTrace(this, this.constructor); }}
And implementations like:
export class NotFoundError extends BaseError { constructor(message = 'Resource not found', code = 'NOT_FOUND') { super(message, code, 404); }}
✅ Use these for infrastructure or API-level errors that need structured HTTP responses.
Example Flow Across Layers
Section titled “Example Flow Across Layers”Step 1: Command Handler
Section titled “Step 1: Command Handler”export class RegisterUserHandler implements CommandHandler<RegisterUserCommand> { async execute(command: RegisterUserCommand): Promise<Result<void, string>> { const result = await this.userService.registerUser(command.email, command.password);
if (result.isFailure()) { return Result.fail(result.error); // Return failure, don’t throw. }
return Result.ok(); }}
Step 2: Controller
Section titled “Step 2: Controller”const result = await commandBus.dispatch(new RegisterUserCommand(email, password));
if (result.isFailure()) { throw new BadRequestError(result.error); // Convert to HTTP-friendly error.}
res.status(201).json({ message: 'User registered' });
Step 3: Global Error Handler
Section titled “Step 3: Global Error Handler”app.use((err, req, res, next) => { if (err instanceof BaseError) { res.status(err.statusCode).json({ code: err.code, message: err.message }); } else { res.status(500).json({ message: 'Internal server error' }); }});
✅ Each layer has clear responsibility.
Best Practices
Section titled “Best Practices”✅ Use Result
for expected, recoverable failures.
✅ Use BaseError
for HTTP or infrastructure-level errors.
✅ Never mix the two inside the domain layer.
✅ Always handle system-level errors with a global error handler.