Skip to content

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).


LayerWhat to Use
Domain LayerResult.ok, Result.fail or domain-specific errors (no throw)
Command / Query HandlersReturn Result (no exceptions)
Controller / API LayerConvert Result.fail to BaseError or HTTP response
Infrastructure FailuresThrow 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.


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.


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.


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();
}
}
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' });
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.


✅ 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.