第24章:例外と失敗の考え方:主処理と付随処理を分ける💥🧠
この章でわかるようになること🎓✨
- 「主処理(例:支払い確定)」と「付随処理(例:メール送信)」を分けて考えられる🙂📌
- 付随処理が失敗しても、主処理を巻き戻さない設計ができる🔁🧩
- 例外(Exception)を「投げるべき/握るべき」の判断ができるようになる🙆♀️🙅♀️
- 失敗を「記録して、あとで直せる」状態にできる🧾🛟
1. まず結論:失敗の“重さ”が違う⚖️😵💫
同じ「失敗」でも、重さが違うんだよね🙂
- ✅ 主処理の失敗:注文確定できない、支払い確定できない → これはユーザー体験・業務ルールに直撃💥
- ⚠️ 付随処理の失敗:メール送信できない、ログが一部残らない → “困るけど、注文は成立していい”ことが多い📧💦
ここを混ぜると… **「メールが送れなかったから支払いも無かったことにします」**みたいな悲劇が起きがち😱🌀
24.2 メイン処理(Primary)と付随処理(Secondary)❤️🎗️

ビジネスとして絶対に成功させたい「メインの仕事」と、失敗しても致命的ではない「脇役の仕事」を分けて考えます。
主処理(ドメインの中心)❤️🔥
「それが成立しないなら、ユースケース自体が失敗」なもの。
例:
- 支払い確定(Paidにする)💳✅
- 発送確定(Shippedにする)📦✅
- 在庫引当(引当できないなら売れない)📉❌
👉 主処理は 不変条件(Invariants) を守りながら状態を変える💎🔐 失敗したら 例外 or 失敗結果 を返して止めてOK🙆♀️
24.1 例外を投げるかどうか?🤔💥

エラーが起きたとき、呼び出し元に例外を伝えて全体の処理を止めるべきか、ログだけ残して進めるべきかを判断します。
付随処理(あとから付いてくるやつ)🎀🧩
「できると嬉しいけど、できなくても主処理は成立していい」ことが多い。
例:
- 支払い完了メール📧
- ポイント付与🎁
- 分析ログ送信📊
- 通知(Slack/Push)🔔
👉 付随処理は ドメインイベントのハンドラ側 に寄せるのが基本🙂🔔
3. 例外(Exception)って“何のため”?🧨🤔
.NETは「失敗」を例外で表す仕組みを持ってるよね。で、例外は乱用すると地獄😇🔥 Microsoftの例外ベストプラクティスでも「例外で通常フローを作らない」「回復できないならキャッチしない」などが整理されてるよ📘✨ (Microsoft Learn)
この教材では、まずこう決めるのがラク👇
✅ 例外を投げていい場面(主処理寄り)💥
- 不変条件違反(支払い前に発送しようとした、金額が不正など)🔐
- 依存してる処理が失敗したらユースケース続行不可(決済APIが落ちた等)💳❌
⚠️ 例外を“握っていい”場面(付随処理寄り)🧤
- メールが送れない📧💦
- ログ送信が失敗📊💦
- 外部通知が失敗🔔💦
握る=「無視」じゃないよ!! 記録して、あとで復旧できる形にするのがセット🧾🛟
4. “主処理が成功したあと”にイベントを配るのが基本🏁🔔
ここ、超だいじ🙂✨
- 主処理で状態変更(例:OrderをPaidにする)
- 保存して主処理を確定(DBコミット相当)
- そのあとにイベントを配信して付随処理を実行
これにすると、付随処理が失敗しても **「支払いは確定してる」**を守れる👍✨
ちなみに2026年1月時点では .NET 10 がLTSで、1/13に 10.0.2 パッチが出てるよ(運用はLTSが無難になりがち)🧩🪟 (Microsoft) そして .NET 10 系の既定C#は C# 14 だよ🧠✨ (Microsoft Learn)
5. 実装例:PayOrder(主処理)→ OrderPaid(イベント)→ 付随処理📦➡️🔔➡️📧
5.1 ドメインイベントの最小形🔔
public interface IDomainEvent
{
DateTimeOffset OccurredAt { get; }
}
public sealed record OrderPaid(Guid OrderId, DateTimeOffset OccurredAt) : IDomainEvent;
5.2 集約ルートに「イベントを溜める」📮🧺(第19章の形)
public abstract class AggregateRoot
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents;
protected void AddDomainEvent(IDomainEvent ev) => _domainEvents.Add(ev);
public void ClearDomainEvents() => _domainEvents.Clear();
}
5.3 Order(主処理の中心)💳🛒
public sealed class Order : AggregateRoot
{
public Guid Id { get; }
public bool IsPaid { get; private set; }
public Order(Guid id)
{
Id = id;
}
public void MarkAsPaid(DateTimeOffset now)
{
if (IsPaid) return; // 2重払い防止(簡易)
IsPaid = true;
// 「起きた事実」をイベントにする🔔
AddDomainEvent(new OrderPaid(Id, now));
}
}
6. アプリケーション層:主処理は“失敗したら止めていい”🛑✅
ここが主処理の責任者🙂✨ 主処理がダメなら、イベント以前にユースケース失敗でOK。
public interface IOrderRepository
{
Task<Order?> FindAsync(Guid orderId, CancellationToken ct);
Task SaveAsync(Order order, CancellationToken ct);
}
public interface IDomainEventDispatcher
{
Task DispatchAsync(IEnumerable<IDomainEvent> events, CancellationToken ct);
}
public sealed class PayOrderService
{
private readonly IOrderRepository _orders;
private readonly IDomainEventDispatcher _dispatcher;
public PayOrderService(IOrderRepository orders, IDomainEventDispatcher dispatcher)
{
_orders = orders;
_dispatcher = dispatcher;
}
public async Task PayAsync(Guid orderId, DateTimeOffset now, CancellationToken ct)
{
var order = await _orders.FindAsync(orderId, ct)
?? throw new InvalidOperationException("Order not found.");
// 主処理:状態変更(ここが失敗するならユースケース失敗でOK)✅
order.MarkAsPaid(now);
// 主処理:保存(ここが失敗したらユースケース失敗)✅
await _orders.SaveAsync(order, ct);
// 付随処理:イベント配信(主処理の確定“後”)🔔
await _dispatcher.DispatchAsync(order.DomainEvents, ct);
// 配り終わったら掃除🧹
order.ClearDomainEvents();
}
}
ポイントはここ👇🙂
- 保存(主処理確定)が終わってからイベント配信してる🏁
- これで「メール失敗で支払いが無かったことになる」を防ぎやすい📧💦➡️🙅♀️
7. ディスパッチャ側:付随処理は“まとめて巻き戻さない”🧯🔁
付随処理で例外が起きても、主処理まで巻き戻す必要がないケースが多いよね🙂 だからディスパッチャ側はこういう発想になる👇
- ハンドラ1個が失敗しても、他のハンドラは実行してOK(ケース多い)🎯
- 失敗は記録して、あとで再実行できるようにする🧾🛟
public interface IDomainEventHandler<in TEvent> where TEvent : IDomainEvent
{
Task HandleAsync(TEvent ev, CancellationToken ct);
}
public interface IFailureStore
{
Task SaveAsync(string eventType, string payloadJson, string reason, DateTimeOffset occurredAt, CancellationToken ct);
}
public sealed class InProcessDomainEventDispatcher : IDomainEventDispatcher
{
private readonly IServiceProvider _sp;
private readonly IFailureStore _failures;
public InProcessDomainEventDispatcher(IServiceProvider sp, IFailureStore failures)
{
_sp = sp;
_failures = failures;
}
public async Task DispatchAsync(IEnumerable<IDomainEvent> events, CancellationToken ct)
{
foreach (var ev in events)
{
// ここはシンプルに:型ごとにハンドラを全部呼ぶ(DIで取る想定)🧩
var handlers = ResolveHandlers(ev);
foreach (var handler in handlers)
{
try
{
await handler(ev, ct);
}
catch (OperationCanceledException)
{
// キャンセルは“失敗”というより中断扱いが多いので、そのまま投げ直しが無難🙂🛑
throw;
}
catch (Exception ex)
{
// ✅ 付随処理の失敗:ここで“主処理を壊さない”判断ができる✨
// でも“無視”ではなく、記録して救えるようにする🧾🛟
await _failures.SaveAsync(
eventType: ev.GetType().FullName ?? "Unknown",
payloadJson: System.Text.Json.JsonSerializer.Serialize(ev),
reason: ex.ToString(),
occurredAt: ev.OccurredAt,
ct: ct
);
// ここでは飲み込む(ベストエフォート)🧤
}
}
}
}
private List<Func<IDomainEvent, CancellationToken, Task>> ResolveHandlers(IDomainEvent ev)
{
// 実装はDI/反射/ジェネリック等いろいろだけど、この章では概念優先🙂
// 例:IEnumerable<IDomainEventHandler<OrderPaid>> を取ってラップする…など
return new();
}
}
8. 「メール失敗で注文も失敗?」の判断フローチャート🧭📧
付随処理が失敗したときの判断、これで迷いが減るよ🙂✨
Q1:それができないと“不変条件が壊れる”?🔐
- YES → 主処理に寄せる(イベントじゃなく主処理の一部)
- NO → 次へ🙂
Q2:ユーザーに即時の保証が必要?(例:画面で「送信完了」表示が必要)🖥️
- YES → 付随処理でも「成功/失敗」を返す設計を検討(ただし運用は難しくなる)⚠️
- NO → 次へ🙂
Q3:失敗しても“あとで復旧”できる?🛟
- YES → 失敗を記録して後で再実行(第25章・第31章に接続)🔁
- NO → そもそも要件・業務側の設計見直しが必要かも🤝💬
9. “一時的エラー”はリトライ候補🌧️🔁(ミニ入門)
メール送信や外部APIは 一時的に落ちるのが普通😇 .NET のレジリエンス(耐障害)では Polly を基盤にした公式のガイド/パッケージも用意されてるよ🧩🛡️ (Microsoft Learn)
ここでは超ミニで「再試行」を雰囲気だけ👇(本格運用は第25章で育てる)
public static class Retry
{
public static async Task RunAsync(Func<Task> action, int maxRetry, TimeSpan delay, CancellationToken ct)
{
for (int i = 0; ; i++)
{
try
{
await action();
return;
}
catch when (i < maxRetry)
{
await Task.Delay(delay, ct);
}
}
}
}
ハンドラ側で:
public sealed class SendPaidEmailHandler : IDomainEventHandler<OrderPaid>
{
private readonly IEmailSender _email;
public SendPaidEmailHandler(IEmailSender email) => _email = email;
public async Task HandleAsync(OrderPaid ev, CancellationToken ct)
{
await Retry.RunAsync(
action: () => _email.SendPaidAsync(ev.OrderId, ct),
maxRetry: 3,
delay: TimeSpan.FromSeconds(2),
ct: ct
);
}
}
※「何でもかんでもリトライ」は危険だよ⚠️ “恒久的に無理”(宛先不正など)までリトライすると地獄👻 ここを次章(第25章)で整理する🧯✨
10. よくある地雷🔥(踏むとつらい)
地雷①:イベント配信を“保存前”にやる💣
- メール送れた✅
- でも保存が失敗して注文は存在しない❌ → 「送ったのに無い」事故😱
👉 保存→配信 の順が基本🏁🔔
地雷②:付随処理の例外で主処理まで失敗にする💥
- メール失敗📧💦
- 支払い確定も無かったことに…💳❌ → ユーザー混乱🌀
👉 付随処理は原則 “記録して後で復旧”🧾🛟
地雷③:catchして何もしない(闇に葬る)🕳️
- 失敗が起きたことすら分からない🙂💦
👉 「最低限のログ+失敗保存」はセット🧾✨
11. Webアプリの例外処理(参考)🌐🧯
Web(ASP.NET Core)では、最終的には例外をまとめてハンドリングして、適切なレスポンスにするよね。最新のASP.NET Core 10 系のエラーハンドリング(UseExceptionHandler など)も更新されてるよ🧩🪟 (Microsoft Learn) ただしこの章の主役は「ドメインイベントで付随処理を巻き戻さない」だから、Webの話は“外側でまとめて受ける”くらいの位置づけでOK🙂👍
12. AIに頼むときのプロンプト例🤖📝✨
- 「OrderPaid のイベントハンドラを3つ(メール、ポイント、監査ログ)に分けて雛形を書いて」📧🎁🧾
- 「付随処理の例外を握りつつ、失敗を FailureStore に保存する実装案を出して」🧤🛟
- 「OperationCanceledException は投げ直す理由も含めてレビューして」🛑🔍
13. 演習🧪🎮✨
演習1:分類ゲーム(主処理?付随処理?)🧠
次を分類してね👇
- 支払い確定 💳
- 支払い完了メール 📧
- 在庫引当 📦
- ポイント付与 🎁
- 分析ログ送信 📊
演習2:設計判断⚖️
「メールが送れないとき、注文は失敗扱い?」
- YES/NO を決めて
- 理由を “不変条件/ユーザー保証/復旧可否” の3観点で書く📝✨
演習3:コード改造🛠️
SendPaidEmailHandler に
- リトライ(最大3回)🔁
- 失敗したら FailureStore 保存🧾 を入れてみよう🙂
まとめ🎀✅
- 主処理は「成立しないなら止めてOK」✅🛑
- 付随処理は「失敗しても主処理を壊さない」設計が基本📧💦➡️🙂
- ただし「握る=無視」じゃない! 記録して復旧できる形にする🧾🛟
- 一時的エラーはリトライ候補🌧️🔁(でも次章で“やりすぎ防止”を学ぶ🧯✨)