• システム開発に関わる内容をざっくりと書いていく

ざっくり C# OperationCanceledException と Timeout の違い(CancellationToken の概念)

非同期処理のログで「止まった」「キャンセルされた」「タイムアウトした」がごちゃまぜになりやすい。C# では、その土台に CancellationToken があり、表面に出る例外が OperationCanceledException だったり、TimeoutException だったりする。

この記事では、CancellationToken の概念から入り、OperationCanceledException と Timeout の違いをざっくり整理する。実装パターンの詳細は CancellationToken でタイムアウトを実装する も参照。


先に結論

  • CancellationToken = 「もうやらなくていい」という合図を下流に渡す仕組み
  • OperationCanceledException(派生の TaskCanceledException 含む)= その合図を受けて処理が止まったとき
  • Timeout = 「時間切れ」。実装によってはキャンセル経路(OCE)になることも、専用の TimeoutException になることもある
  • HttpClient のタイムアウトは、しばしばキャンセル例外として表面化する
  • ログでは「誰が Cancel したか/時間切れか」を区別できるメッセージを残す

CancellationToken の概念

非同期メソッドが「途中でやめていい」状態を知るための仕組みが CancellationToken

  • CancellationTokenSource(CTS) … Cancel を出す側(合図の発生源)
  • CancellationToken … Cancel を見る側(メソッド引数に渡す)
  • CTS で Cancel すると、その Token を受け取っている処理が「終了してよい」とわかる
using var cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;

// 別スレッドやタイマーで Cancel
cts.Cancel();

// または時間切れで Cancel
cts.CancelAfter(TimeSpan.FromSeconds(10));

何のため?

  • ユーザーが画面を閉じた・リクエストが中断された
  • アプリがシャットダウンする
  • 呼び出し側が「もう結果はいらない」と判断した
  • 一定時間で打ち切りたい(タイムアウトも Cancel として表現できる)

ポイントは、例外を投げること自体が目的ではないこと。まず Token で「やめて」を伝え、処理がそれに気づいて止まった結果として OperationCanceledException が出ることが多い。

トークンの渡し方(ざっくり)

  • async メソッドの引数に CancellationToken ct = default を付ける
  • HttpClient / DB / 自前ループまで同じ Token を下流に渡す
  • ASP.NET Core では HttpContext.RequestAborted が「クライアント切断」の Token
  • 呼び出し元の Token と自前タイムアウトを両立するときは CreateLinkedTokenSource
// 親のキャンセル + 10秒タイムアウト、どちらかで止まる
using var linked = CancellationTokenSource.CreateLinkedTokenSource(parentCt);
linked.CancelAfter(TimeSpan.FromSeconds(10));
await DoWorkAsync(linked.Token);

OperationCanceledException と Timeout の違い

表面の例外だけ見ると紛らわしいので、何が起きたかで分ける。

概念                         意味                         よく出る例外
────────────────────────────────────────────────────────────────────────
CancellationToken            「やめて」の合図              (まだ例外ではない)
OperationCanceledException   合図を受けて止まった          OCE / TaskCanceledException
Timeout(時間切れ)          制限時間を超えた              TimeoutException のことも
                                                         キャンセル経路(OCE)のことも
  • キャンセル … 誰かが意図的に「不要」と伝えた結果。必ずしもバグではない
  • タイムアウト … 「待ちすぎた」。業務的には遅延・設定不足・外部障害の切り分け対象になりやすい
  • タイムアウトを CancelAfter で実装すると、時間切れでも例外型は OCE 系になる

例外型の整理

  • OperationCanceledException … キャンセル結果の基本型
  • TaskCanceledException … OCE の派生。async / Task 周りでよく見る
  • TimeoutException … 明示的な時間切れ API が出すことがある(ライブラリ依存)
  • 同じ「時間切れ」でも、ライブラリごとに例外型が違う

HttpClient の落とし穴

HttpClient.Timeout や Token 経由の打ち切りは、しばしば TaskCanceledException として表面化する。ログに「Canceled」とだけ出ても、中身は時間切れのことがある。


現場での見方

  • リクエスト中断・アプリ停止・連携キャンセルなら OCE は想定内になりうる(エラー率に混ぜすぎない)
  • 外部 API 待ちで時間切れなら、設定値・リトライ・相手側遅延を疑う
  • IsCancellationRequested や Token の紐付け元(HttpContext / 親 Token / CancelAfter)を確認
  • キャッチして握りつぶすと、本当の障害を隠す。種類で分岐する
try
{
    await DoWorkAsync(ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
    // 呼び出し元が Cancel した想定内
    throw;
}
catch (OperationCanceledException)
{
    // Token はまだ生きているのに OCE → 別経路の時間切れなどの可能性
    // ログに「timeout-like cancel」などと残す
    throw;
}

ざっくりまとめ

  • CancellationToken は「やめて」の合図を下流に渡す仕組み
  • その結果が OperationCanceledException(派生含む)
  • タイムアウトは「時間切れ」。CancelAfter で実装すると例外はキャンセル側になることが多い
  • ログには「誰が Cancel したか」と経過時間を残す

関連記事