C# の async Task メソッド内で foreach を回している最中に、条件を満たしたら処理を打ち切りたい——そんなときに迷うのが break と return の使い分けです。本記事では Task.CompletedTask の位置づけも含め、意図どおりに終了させる判断基準と実装例をまとめます。
結論:やりたいことが「メソッド終了」か「ループ終了」かで決まる
最初に結論を整理します。async Task であっても、break と return の役割は同期メソッドと同じです。「どこまで処理を終わらせたいか」で選びます。
| やりたいこと | 使うキーワード | 何が終わる? | ありがちな使いどころ |
|---|---|---|---|
| 条件を満たしたら、以降の処理を一切せずに呼び出し元へ戻りたい | return; | メソッド全体 | 「ここで終わり」が確定している(エラー、検出、終了条件など) |
| ループは止めたいが、ループ後の後処理(ログ、集計、クリーンアップなど)は続けたい | break; | 現在のループ | ループ結果を使って後続処理を行う、最後に必ず後処理をしたい |
| 「非同期メソッドだから何か await しないといけない気がする」ので、とりあえず待ちたい | await Task.CompletedTask; | ほぼ何も変わらない | この用途では不要。設計意図がある場合を除き避ける |
つまり、質問の「ある条件を満たしたら戻りたい」がメソッド自体を終えたい意味なら return; が正解です。ループだけ抜けたいなら break; を選びます。
前提:質問のコードで何が起きているか
質問のイメージは次のような形です。
public async Task MyMethod()
{
string[] lists = GetCustomerLists();
foreach (string lst in lists)
{
// いろいろ処理
// ある条件を満たしたら「戻りたい」
????
}
}
ここで迷いやすいポイントは、「async Task って Task を返すんだから、return; だけでいいの?」「Task.CompletedTask を返したり await したりする必要があるの?」という不安です。結論から言うと、この場面では return; と break; の選択だけで足りることがほとんどです。
なぜ async Task でも return; で終われるのか
async メソッドは、コンパイラが裏側で「状態(ステート)を持つ仕組み」に変換し、呼び出し元に Task(完了を表す入れ物)を返します。重要なのは次の2点です。
return;は「完了」を意味する:async Task(戻り値なし)の場合、return;でメソッドを抜けると、コンパイラが完了済みの Task を返したのと同等に扱ってくれます。- 例外は Task に乗る:
asyncメソッド内で例外が投げられると、呼び出し側から見た Task は「失敗(Faulted)」になります。awaitしている側では、その例外が再スローされます。
そのため、「非同期だから Task を明示的に返さないといけない」という発想はここでは不要です。return; と書けば、コンパイラが「この Task は完了した」として扱ってくれます。
もう少し実務に近い言い方をすると、async Task は最後まで実行し切るか、途中で return; するか、例外で落ちるかのどれかで Task の状態が決まります。これが理解できると、Task.CompletedTask をわざわざ書かなくてよい理由も自然に腑に落ちます。
メソッド自体を途中で終了したいなら return;
「条件を満たしたら処理を打ち切り、MyMethod 自体を終わらせたい」なら return; を使います。ループの途中であっても、メソッドの外に一気に戻れます。
public async Task MyMethod()
{
string[] lists = GetCustomerLists();
foreach (string lst in lists)
{
// 条件を満たしたらメソッドを終了
if (ShouldStop(lst))
{
return; // MyMethod 全体がここで終了(完了済み Task を返すのと同等)
}
await DoSomethingAsync(lst);
}
// ここには到達しない
}
ポイントは、return; が「非同期らしくない」わけではない、ということです。await が存在するかどうかとは関係なく、return; は「これ以上続けない」という明確な意思表示になります。
return; しても finally や using は動く
「途中で return したら後始末が飛ぶのでは?」と心配する方もいますが、C# は return / break / 例外などでスコープを抜けるとき、finally ブロックや using による破棄処理を実行します。async でも基本は同じです。
public async Task MyMethod()
{
using var scope = new SomeScope(); // IDisposable の例
foreach (var lst in GetCustomerLists())
{
if (ShouldStop(lst))
{
return; // scope.Dispose() は呼ばれる
}
await DoSomethingAsync(lst);
}
}
ただし、IAsyncDisposable を非同期に破棄したい場合は await using を使います(C# 8 以降)。
public async Task MyMethod()
{
await using var scope = new AsyncScope(); // IAsyncDisposable の例
foreach (var lst in GetCustomerLists())
{
if (ShouldStop(lst))
{
return; // scope.DisposeAsync() が await される
}
await DoSomethingAsync(lst);
}
}
「途中終了したいけど、必ず後処理をしたい」場合は return を避けるのではなく、try/finally(または using / await using)で後処理を保証するのが筋のよい設計です。
return; を使うと読みやすいケース
実務では、次のような場合に早期 return が特に効きます。
- 前提条件が満たされない(入力不正、設定不足、対象が存在しない)
- 致命的な状態を検出した(DB 接続不可、認証失敗など)
- 「見つけたら終わり」(検索・探索系)
- 「成功したら終わり」(リトライや複数候補の試行)
こうした場面では、無理にフラグ変数でループを続けるより、return でスパッと終わらせる方がコードが短く、意図もはっきりします。
ループだけ抜けて後続処理を続けたいなら break;
「ループは打ち切りたいが、このメソッドとしてはまだやることがある」なら break; が適切です。break はあくまで foreach(または for / while)から抜けるだけで、メソッドの実行は続きます。
public async Task MyMethod()
{
string[] lists = GetCustomerLists();
foreach (string lst in lists)
{
if (ShouldStop(lst))
{
break; // ループだけ抜ける
}
await DoSomethingAsync(lst);
}
// ここには到達する
await AfterLoopAsync();
}
break; を選ぶべき典型パターン
- ループの後に必ず実行したい後処理がある(ログ、通知、集計、リソース解放など)
- 「途中までの結果」を使って後で判断したい(例:処理件数が一定なら別の処理へ)
- ループ終了理由によって分岐したい(停止理由を変数に持っておく)
特に最後の「終了理由で分岐したい」は、break と相性が良いです。単に break するだけでなく、「なぜ抜けたのか」を残すと保守性が上がります。
public async Task MyMethod()
{
bool stoppedByCondition = false;
foreach (var lst in GetCustomerLists())
{
if (ShouldStop(lst))
{
stoppedByCondition = true;
break;
}
await DoSomethingAsync(lst);
}
if (stoppedByCondition)
{
await NotifyStoppedAsync();
}
else
{
await NotifyCompletedAsync();
}
}
「メソッドから戻りたい」という意図で break を使ってしまうと、ループ後の処理が走ってしまい、想定外の副作用につながります。ループ後に何があるかを見て、本当に break でいいのかを必ず確認してください。
await Task.CompletedTask; はこの用途では不要
結論として、質問の「条件を満たしたら処理を打ち切りたい」という用途で await Task.CompletedTask; を使う必要はありません。理由はシンプルで、Task.CompletedTask は「すでに完了している Task」だからです。
awaitは「未完了なら待つ」ための構文- 完了済みの Task を
awaitしても、基本的に待ちが発生しない(その場で終わる) - つまり
await Task.CompletedTask;は、多くのケースで意味のない 1 行になりやすい
「非同期メソッドなのだから、最後に await Task.CompletedTask; を書いておけば安心」という発想は、可読性を下げるだけになりがちです。読み手は「ここで何か待つ理由があるのかな?」と誤解しますし、レビュー時に説明が必要になります。
例外:Task.CompletedTask が登場する「正しい」場面
Task.CompletedTask 自体が悪いわけではありません。次のように、「async ではないが Task を返したい」ときに便利です。
// await が一切ないなら、async を付けない選択肢がある
public Task MyMethod()
{
// 同期処理だけで完了する
DoSyncWork();
return Task.CompletedTask;
}
async Task にしてしまうと、コンパイラは状態マシン生成などのオーバーヘッドを持ちます。実際には非同期処理がないのに async を付けると、次のような警告(代表例:CS1998)が出ることがあります。
- 「この async メソッドには await がなく、同期的に実行されます」
この場合は、async を外して Task.CompletedTask を返す方が、意図が明確で効率的です。「とりあえず await を書いて警告を消す」ために await Task.CompletedTask を置くのは避けた方が無難です。
「意図的に一度だけ非同期にしたい」なら Task.Yield() を検討する
もし「必ず一度は非同期にして、呼び出し元に制御を返したい」という明確な意図があるなら、Task.CompletedTask ではなく Task.Yield()(または状況に応じた待機)を検討します。
public async Task MyMethod()
{
// 意図的に一度だけスケジューラに制御を返す
await Task.Yield();
// ここから先は後続処理
await DoSomethingAsync();
}
ただし Task.Yield() は UI アプリや一部の特殊な状況で意味が出るもので、乱用すると逆にパフォーマンスや追跡性が悪化します。「何となく非同期っぽくしたい」という理由では使わない方が安全です。
Task<T> を返す場合:return 値; で早期終了する
戻り値がある非同期メソッド(Task<T>)の場合、return 値; で T を返します。ここでも、値を包んで Task<T> にするのはコンパイラの仕事です。
public async Task<int> CountAsync()
{
int count = 0;
foreach (var lst in GetCustomerLists())
{
if (ShouldStop(lst))
{
return count; // Task<int> の T にあたる int を返す
}
count += await CountOneAsync(lst);
}
return count;
}
Task<T> では、return;(式なし)は書けません。必ず T の値を返す必要があります。逆に Task(戻り値なし)では、return; だけでよい、という違いがポイントです。
「途中で止めたい」を設計で扱う:実務向けの考え方
ループの途中終了は、単に break / return を選ぶだけでなく、「なぜ止めたいのか」「誰が止めるのか」を整理すると設計が安定します。ここではよくある要件別にパターンを紹介します。
内部条件で止める:return と break を素直に使う
停止条件がメソッド内部で完結しているなら、シンプルに書くのが最善です。
- 後続処理が不要なら
return; - 後続処理が必要なら
break;(またはフラグを立ててから break)
このパターンで重要なのは、「停止後に何をしたいのか」を最初に決めることです。途中で仕様が変わりやすい場合は、後処理を finally に寄せると修正が楽になります。
外部要因で止める:CancellationToken を使う
「ユーザーがキャンセルした」「サーバー側がタイムアウトさせたい」「上位処理から止めたい」といった外部要因で止める場合は、CancellationToken を受け取る設計が定石です。単に return で抜けるよりも、呼び出し側と合意された形で終了できます。
public async Task MyMethodAsync(CancellationToken ct)
{
foreach (var lst in GetCustomerLists())
{
ct.ThrowIfCancellationRequested(); // キャンセル要求があれば例外で終了
if (ShouldStop(lst))
{
return; // 内部条件による終了
}
await DoSomethingAsync(lst, ct);
}
}
キャンセルを「例外(OperationCanceledException)」として扱うのが一般的なのは、呼び出し側で await したときに「キャンセルだった」と判定しやすいからです。もちろん、アプリの方針によっては return で静かに終える設計もありますが、その場合でも「キャンセルで終わったのか、正常終了なのか」が分かるように戻り値(結果型)を設計すると混乱が減ります。
タイムアウトで止める:CancelAfter を組み合わせる
処理全体にタイムアウトを設けたい場合は、CancellationTokenSource の CancelAfter を使うと実装が素直になります。
public async Task MyMethodAsync(CancellationToken ct)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(10));
foreach (var lst in GetCustomerLists())
{
cts.Token.ThrowIfCancellationRequested();
await DoSomethingAsync(lst, cts.Token);
}
}
「途中で止めたい」が性能やUXの要件から来ている場合、break / return だけでなく、キャンセルやタイムアウトを先に設計として用意しておくと、将来的に仕様が増えても対応しやすくなります。
並列化している場合:return しても裏で Task が走り続けることがある
質問のコードは await DoSomethingAsync(lst) のように逐次待機している想定なので、return すれば「それ以降は何も実行されない」状態になります。ただし実務では、次のような非 await の Task(いわゆる fire-and-forget)を作っていると、return しても処理が裏で継続することがあります。
public async Task MyMethod()
{
foreach (var lst in GetCustomerLists())
{
// await しない Task を開始(注意)
_ = DoSomethingAsync(lst);
if (ShouldStop(lst))
{
return; // ここで戻っても、開始した Task は裏で動き続ける
}
}
}
この形は、例外が握りつぶされたり(未観測例外)、リソース競合が起きたり、アプリ終了時に中途半端な状態になったりと、トラブルの温床になります。並列に走らせたいなら、次のようにTask を収集して最後に待つか、キャンセルを設計に組み込みます。
public async Task MyMethodAsync(CancellationToken ct)
{
var tasks = new List<Task>();
foreach (var lst in GetCustomerLists())
{
ct.ThrowIfCancellationRequested();
tasks.Add(DoSomethingAsync(lst, ct));
if (ShouldStop(lst))
{
break; // ここでは break して、最後にまとめて待つ設計もあり
}
}
await Task.WhenAll(tasks);
}
「途中で止めたい」の真意が「並列処理を止めたい」なのか「単にループをやめたい」なのかで、最適解は変わります。前者なら CancellationToken とタスク管理が重要です。
break / return / continue / throw の使い分け早見表
途中終了の文脈では、continue や throw も含めて整理しておくと迷いが減ります。
| キーワード | 効果 | 向いている用途 | 注意点 |
|---|---|---|---|
continue; | 現在のループの残りをスキップして次の反復へ | 「この要素は処理対象外」など、スキップしたい | 意図が曖昧だと読み手が迷う。条件は分かりやすく書く |
break; | 現在のループを終了して次の文へ | 「これ以上ループする意味がない」 | メソッドは終わらない。後続処理が走ることを忘れない |
return; | メソッドを終了して呼び出し元へ | 「ここで処理を終える」 | 後続処理が必要なら break + 後処理、または try/finally を検討 |
throw; | 例外で終了(Task は Faulted になる) | 異常終了を明確に伝えたい | 例外設計(例外型・メッセージ・ログ)が重要。キャンセルは別扱い |
よくある勘違い・落とし穴と対処法
「break でも return でも動くように見える」ことがあるのが、このテーマのややこしい点です。ありがちな落とし穴を先に知っておくと、バグの芽を潰せます。
| 症状 | 原因 | 対処 |
|---|---|---|
| 「条件を満たしたら止めたつもり」なのに、ループ後の処理が走ってしまう | break を使っていてメソッドが終了していない | メソッドを終えたいなら return;。後処理が必要なら構造を見直す |
await Task.CompletedTask; を書いたが、動作が変わらない | 完了済み Task を待っているだけで実質 no-op | 目的が「終了」なら return / break。目的が「一度非同期」なら Task.Yield 等を検討 |
| 非同期処理を止めたつもりが、裏で走り続ける | 開始した Task を await していない(fire-and-forget) | Task を収集して WhenAll で待つ、またはキャンセル可能に設計する |
| 例外がどこで起きたか分かりにくい | ループ内で例外を握りつぶしている / ログがない | 例外は基本的に上に伝える。握るなら意味のあるログとリカバリ方針を用意 |
| メソッドが async なのに await がなく警告が出る | 非同期処理が実際には存在しない | async を外して Task.CompletedTask を返す。将来のために残すなら理由をコメントで明確に |
実装をスッキリさせる小技:処理を分割して「戻りたい場所」を明確にする
ループの途中で戻る条件が複雑になると、break と return の混在で読みづらくなることがあります。そんなときは、処理をメソッドに分割して「どこまでがループの責務か」をはっきりさせると見通しが良くなります。
public async Task MyMethodAsync()
{
var result = await ProcessListsAsync();
// ここから先は「ループとは別の責務」に分離
await AfterProcessAsync(result);
}
private async Task<ProcessResult> ProcessListsAsync()
{
foreach (var lst in GetCustomerLists())
{
if (ShouldStop(lst))
{
return ProcessResult.Stopped;
}
await DoSomethingAsync(lst);
}
return ProcessResult.Completed;
}
こうしておくと、途中終了は ProcessListsAsync の中で完結し、呼び出し側は結果に応じた後処理を書くだけになります。規模が大きくなるほど、こうした分割は効果が出ます。
まとめ:async Task の途中終了は「同期と同じ感覚」でOK
- メソッド自体を途中で終えたいなら
return;(async Taskでも明示的に Task を返す必要はない) - ループだけ抜けて後続処理を続けたいなら
break;(ループ後の処理が走ることを前提に設計する) await Task.CompletedTask;はこの目的では不要(意味のある待機ではない)- 「外部から止めたい」「並列処理も止めたい」なら CancellationToken とタスク管理を合わせて考える
async は「await できる」仕組みを提供するだけで、break と return の基本ルールを変えるものではありません。意図(どこまで止めるか)を先に決め、コード構造でそれをはっきり表現するのが、最短でバグを減らすコツです。

コメント