DDDで例外をどう扱う?
失敗を判断した層から出す
DDDには、例外の使い方を一つに決めたルールはありません。どこで問題が分かったかによって、例外を出す場所を決めます。
業務ルールを守れない
ルールを確認したEntity、Value Object、Domain Serviceから、業務上の失敗を表す例外を出します。
対象が見つからない
Repositoryの取得結果を確認したApplication Serviceから、対象が見つからないことを表す例外を出します。
DBや外部APIで障害が起きた
Infrastructure層で発生した例外として扱います。DBやライブラリ固有の例外をDomain層から出しません。
HTTPのエラーレスポンスへ変換する
Presentation層が例外の種類を確認し、HTTPステータスとレスポンスへ変換します。
業務ルールを守れない場合
例えば、申込期限を過ぎた募集へ参加を申請した場合を考えます。申込期限を確認するのは「募集」を表すAggregate Rootなので、RecruitmentEntityから例外を出します。
// 申込期限を過ぎたことを表すDomain層の例外
export class ParticipationDeadlineExceededError extends Error {
readonly code = "PARTICIPATION_DEADLINE_EXCEEDED";
constructor() {
super("申込期限を過ぎています");
this.name = "ParticipationDeadlineExceededError";
}
}
export class RecruitmentEntity {
requestParticipation(input: {
requesterId: ParticipantIdValueObject;
now: Date;
}): void {
// 状態を変更する前に、業務ルールを確認する
if (this.deadline.hasPassed(input.now)) {
throw new ParticipationDeadlineExceededError();
}
this.participantIds.push(input.requesterId);
}
}例外を出す前に一部の状態を変更すると、失敗した後に不正な状態が残る可能性があります。必要なルールを先に確認し、すべて満たした場合だけ状態を変更します。
対象が見つからない場合
「募集が存在すること」は募集自身が判断する業務ルールではありません。Repositoryから取得できなかったことを確認したApplication Serviceから例外を出します。
const recruitment = await this.repository.findById(
command.recruitmentId,
);
if (!recruitment) {
throw new RecruitmentNotFoundError(command.recruitmentId);
}
recruitment.requestParticipation({
requesterId: participantId,
now: command.requestedAt,
});APIでHTTPレスポンスへ変換する
Domain層の例外には、HTTPステータスを持たせません。APIが例外を受け取り、外部へ返す形式へ変換します。
try {
await applicationService.execute(command);
return Response.json({ success: true });
} catch (error) {
if (error instanceof ParticipationDeadlineExceededError) {
return Response.json(
{ code: error.code, message: "申込期限を過ぎています" },
{ status: 409 },
);
}
// 想定していない障害は握りつぶさない
throw error;
}例外ではなくResult型を使う場合
期限切れや定員到達が頻繁に起こり、呼び出し側が結果によって処理を分ける場合は、例外ではなくResult型で返す方法もあります。
type RequestParticipationResult =
| { success: true }
| {
success: false;
reason: "deadlineExceeded" | "capacityReached";
};例外とResult型のどちらを使うかより、業務ルールをドメインモデルで確認し、失敗しても不正な状態を残さないことが重要です。入力項目をまとめて確認して複数のエラーを返したい場合も、Result型やNotificationを使う方が扱いやすくなります。
ディレクトリで表す
src/features/futsal/
├── domain/
│ └── recruitment/
│ ├── RecruitmentEntity.ts
│ └── errors/
│ └── ParticipationDeadlineExceededError.ts
├── application/
│ └── errors/
│ └── RecruitmentNotFoundError.ts
└── presentation/
└── api/
└── requestParticipationRoute.ts