no-thrown-failure
An expected business failure is a value: the aggregate returns it in a Result, and exceptions stay for bugs.
- Rule
tactical/no-thrown-failure- Category
- Tactical: how building blocks are written
- Reports
- A public method of an aggregate or entity that returns no
Result, a thrownDomainError - Applies to
- Classes that extend
AggregateRootorEntity;throwanywhere in the project - Turn off
"tactical/no-thrown-failure": "off"
Why
order.place() throws InvalidTotal when the total is zero. Nothing in its signature says so: the controller that calls it does not catch it, and a customer gets an error 500 for a mistake they could have fixed.
The fix
The method returns Result<void, InvalidTotal>. The failure is in the type, and TypeScript makes every caller deal with it before it reaches the value. A method that changes state says whether it worked; reads are getters.
What it checks
AggregateRoot or Entity, even when its return type is inferred. Getters, static methods, toSnapshot and equals are left out.What it reports
src/ordering/domain/aggregates/order.aggregate.ts:2
tactical/no-thrown-failure: Order.place must return a Result:
expose reads as getters and return business failures as
values.
src/ordering/domain/aggregates/order.aggregate.ts:4
tactical/no-thrown-failure: A DomainError is thrown: return
it in a Result instead.Fix it
Return the failure in a Result
So that the caller sees what can go wrong, return the domain error with err(…) and success with ok(), and declare both in the return type.
export class Order extends AggregateRoot<OrderId> {
place(total: number): void {
if (total <= 0) {
throw new InvalidTotal({ total });
}
}
}export class Order extends AggregateRoot<OrderId> {
place(total: number): Result<void, InvalidTotal> {
if (total <= 0) {
return err(new InvalidTotal({ total }));
}
return ok();
}
}Expose reads as getters
So that the public methods are the ones that change state, a read without parameters is a getter, which the rule leaves out.
isPlaced(): boolean {
return this.status === "placed";
}get isPlaced(): boolean {
return this.status === "placed";
}A read that needs parameters, such as canShip(date), returns ok(…), or moves to a DomainService when it involves more than the aggregate.
Turn it off
rules: { "tactical/no-thrown-failure": "off" },On an existing project, prefer a baseline: new methods return a Result while you convert the old ones.
See also
- Result and Domain errors, what the methods return
- Aggregates, whose business methods this rule checks
tactical/no-plain-class, which keepsextends Errorout of the domain- Rules, every rule by category