アーリーリターン(early return、早期リターン)は、処理の途中で結果が確定した場合などに、return で関数から早く抜ける書き方です。異常系を先に返すと、本来の処理が if の奥に沈まず、関数を上から下へ一直線に読めます。この記事では、ネストした条件分岐の書き換え方、ループでの continue、6言語での書き方、リンターによる不要なelseの検出と自動修正の限界を実測付きで示します。後半では「returnは1つにすべき」という単一出口原則と、Appleのgoto failバグから分かる使うべきでない場面も扱います。
まとめ
アーリーリターンは、本筋の処理に進まないケースを先に返し、正常系をインデントの浅い位置に残す手法です。こうして先に判定して抜ける if ...: return をガード節(guard clause)と呼びます。この書き方は『リーダブルコード』7.5節「関数から早く返す」とファウラーのリファクタリングカタログで紹介され、Go・CakePHPのコーディング規約も推奨しています。
- 使う場面:引数の検証・対象外データの除外、本筋から外れる特別なケースの先出し。ループでは
continueが同じ役割を担う。 - 効果:手元の計測で、ifが3段に入れ子になった関数を書き換えると認知的複雑度は9から3に下がった。一方、サイクロマティック複雑度は4のまま変わらない。
- 検出:ESLintの
no-else-return、PylintのR1705、RuffのRET505。ただし自動修正では条件の反転まではしてくれない。 - 避ける場面:A・B・Cのように対等な場合分け、後片付けを手書きする処理、波かっこを省いて次の行に書くガード節。
アーリーリターン(早期リターン)の意味とガード節との関係
アーリーリターンとは、関数の途中(多くは冒頭)で条件を判定し、その先が不要と分かった時点で return する書き方です。「アーリーリターン」「早期リターン」「early return」「早期return」は同じものを指す表記ゆれです。英語圏では「return early」「bail early」とも言います。
関数から早く返す書き方を解説しているのは、『リーダブルコード』(オライリー・ジャパン)の第7章「制御フローを読みやすくする」にある7.5節「関数から早く返す」です。書籍全体の要点はリーダブルコードとは?読みやすいコードの4原則と書籍のエッセンスで整理しています。
ガード節(読み方は「ガードせつ」)は、通常の処理に進まない条件を先に判定して return や例外送出で抜ける部分を指す、リファクタリングの用語です。マーチン・ファウラーのカタログには「Replace Nested Conditional with Guard Clauses」として載っています。そこでは、isDead・isSeparated・isRetired の各条件に応じて結果を変数へ代入していく入れ子の条件分岐を、条件ごとに即 return して最後に通常の支給額を返す形へ置き換えています。例が示すとおり、ガード節は入力の検証だけでなく、本筋から外れる特別なケースの結果を先に返す用途にも使います。アーリーリターンは「途中で抜ける」書き方全般を指し、ガード節はその代表的な使い方です。リファクタリング全体の進め方はリファクタリングとは?24の観点とタイミング・進め方を参照してください。
ネストした条件分岐からアーリーリターンへの書き換え手順
正常系を if の内側へ進めていく書き方では、条件が1つ増えるたびにインデントが1段深くなります。次のPythonコードは、注文を検証してから受け付ける関数です。
def process_order_nested(order):
if order is not None:
if "items" in order:
if len(order["items"]) > 0:
return "受注OK"
else:
return "エラー: 商品が空です"
else:
return "エラー: itemsキーがありません"
else:
return "エラー: 注文がNoneです"
正常系の "受注OK" はインデント4段目に埋もれています。しかも、各 else は対応する if から離れた場所にあります。「注文がNone」のエラーは関数の最後の行にあり、対応する条件を判定している、関数本体の最初の行から最も遠くなっています。
条件を反転させ、失敗したらその場で返すように書き換えます。
def process_order_guard(order):
if order is None:
return "エラー: 注文がNoneです"
if "items" not in order:
return "エラー: itemsキーがありません"
if len(order["items"]) == 0:
return "エラー: 商品が空です"
return "受注OK"
条件とその結末が同じ場所に並ぶので、対応する else を探す必要がなくなります。最後の return "受注OK" は「ここまでの関門をすべて通過した」という意味になります。新しい検証を足すときも、既存のネストを組み替えずに if ...: return を1つ挟むだけです。None・空の辞書・空の items・商品ありの4通りを入力し、2つの関数が同じ値を返すことを確認しています。
書き換えの手順は次の3つです。
- 一番外側の
ifの条件を反転する(is not Noneをis Noneにする)。 - その
elseの中身を反転したifの直下に移してreturnする。 - 元の
ifの中身をインデント1段分戻し、内側のifにも同じ手順を繰り返す。
ループ内のネストを減らすcontinueの使い方
ループの中でも同じ考え方が使えます。処理しない要素を continue で先に飛ばす書き方で、アーリーコンティニュー(early continue)と呼ばれます。
def total_nested(items):
total = 0
for item in items:
if item is not None:
if item["qty"] > 0:
total += item["price"] * item["qty"]
return total
def total_continue(items):
total = 0
for item in items:
if item is None:
continue
if item["qty"] <= 0:
continue
total += item["price"] * item["qty"]
return total
「関数を抜ける」のではなく「その要素の処理だけを打ち切る」点が return と違います。ループの後に集計や後片付けが続く関数では、return と書き間違えると残りの要素がすべて無視されます。ループ全体を打ち切りたいときは break を使います。どれを使うかは、どこまで抜けたいかで決めます。
言語別のアーリーリターンの書き方
アーリーリターンは言語の機能ではなく、return を持つ言語ならどこでも書けます。ただし、言語によっては専用の構文があったり、規約が書き方を決めていたりします。以下は動作確認済みの関数部分の抜粋です。Javaでは java.io と java.nio.file の該当クラス、Goでは os と strings のインポートが必要です。Rustでは is_active: bool と rank: i32、Swiftでは isActive: Bool と rank: Int を持つ User 型を別途定義しています。
Pythonのガード節とwithによるファイルの後片付け
Pythonは if 条件: の次の行に return を書くだけです。None の判定には is None を使い、空のコレクションは if not items: で弾けます。前の章の process_order_guard がそのまま基本形です。ファイルを扱う関数は with 文の中で返せば、どこで抜けてもファイルが閉じられます。
JavaScript・TypeScriptのガード節と波かっこの扱い
次の例はJavaScriptです。TypeScriptで noImplicitAny を有効にする場合は、引数に型注釈が必要です。例えば、User 型を定義したうえで getDiscount(user: User | null | undefined): number と宣言します。!user は null と undefined をまとめて弾きます。
function getDiscount(user) {
if (!user) return 0;
if (!user.isActive) return 0;
return user.rank * 100;
}
1行で書くなら、return は条件と同じ行に置きます。条件の次の行へ波かっこなしで書くと、後で行を足したときに条件の外へはみ出します。これは後半の章で扱うgoto failと同じ構造です。チームで書き方を揃えるなら、ESLintの curly ルールで波かっこを必須にする方法もあります。
Javaの早期returnとtry-with-resources
Javaでは、すべての return が宣言した型を返す必要があります。void メソッドなら、値のない return; がガード節になります。リソースを扱うときは、try-with-resourcesの中でアーリーリターンしても close() が呼ばれます。
static String firstLine(Path path) throws IOException {
try (BufferedReader r = Files.newBufferedReader(path)) {
String line = r.readLine();
if (line == null) return "";
return line.trim();
}
}
空のファイルを渡すと "" が返り、1行目が " hello " なら "hello" が返ることをOpenJDK 25.0.4.1で確認しています。
Rustのlet … elseと?演算子による早期リターン
Rustには、ガード節を宣言的に書く let ... else があります。2022年11月3日リリースのRust 1.65.0で安定化しました。パターンに一致しなければ else ブロックへ進み、そこで return などにより必ず抜けなければなりません。
fn get_discount(user: Option<&User>) -> i32 {
let Some(user) = user else { return 0; };
if !user.is_active { return 0; }
user.rank * 100
}
fn read_config(path: &str) -> Result<String, io::Error> {
let text = fs::read_to_string(path)?;
Ok(text)
}
Result や Option を返す関数では、? 演算子もアーリーリターンとして働きます。Result なら Err を、Option なら None をその場で呼び出し元へ返します。存在しないパスを渡すと Err(NotFound) が返ることをrustc 1.99.1で確認しています(use std::fs; と use std::io; が前提です)。
Goのエラー処理とdeferによる後片付け
Go Code Review Commentsの「Indent Error Flow」は、「通常の処理経路のインデントを最小に保ち、エラー処理を先にインデントして片付ける」よう求めています。if err != nil { return ... } を並べるのはこのためです。
func readName(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
buf := make([]byte, 64)
n, err := f.Read(buf)
if err != nil {
return "", err
}
return strings.TrimSpace(string(buf[:n])), nil
}
defer f.Close() は、ファイルを開けたと確認できた後に書きます。こうすると、以降のどの return で抜けても閉じられます。この例は最大64バイトを1回だけ読み取り、空ファイルでは io.EOF を返します。ファイル全体を読む用途では、読み取りを繰り返すか io.ReadAll などを使います。
Swiftのguard文とスコープからの退出要件
Swiftには、ガード節専用の guard 文があります。言語リファレンスは、else 節で Never を返す関数を呼ぶか、return・break・continue・throw でスコープの外へ制御を移すことを求めています。
func getDiscount(_ user: User?) -> Int {
guard let user else { return 0 }
guard user.isActive else { return 0 }
return user.rank * 100
}
else 節に print だけを書いて抜けないと、Swift 6.3.3ではビルドが 'guard' body must not fall through というエラーで止まります。抜け忘れをコンパイラが検出してくれるので、ガード節を書き損じにくい言語です。
複雑度の指標で見るアーリーリターンの効果
制御フローの複雑さの変化は、指標で比較できます。ただし、指標が下がることだけで読みやすさを保証できるわけではありません。前の章の4つの関数を、サイクロマティック複雑度(radon 6.0.1)と認知的複雑度(flake8-cognitive-complexity 0.1.0、内部ライブラリcognitive-complexity 1.3.0)で計測しました。このライブラリはSonarSourceの原アルゴリズムと完全には一致しないため、表はこの実装での測定値です。
| 関数 | サイクロマティック複雑度 | 認知的複雑度 |
|---|---|---|
| process_order_nested(ifが3段) | 4 | 9 |
| process_order_guard(ガード節) | 4 | 3 |
| total_nested(ループ内ネスト) | 4 | 6 |
| total_continue(continue) | 4 | 5 |
今回の書き換えでは判定の数が変わらないため、サイクロマティック複雑度も変わりません。ただし、論理演算子やループなども加算対象なので、if の数だけで値は決まりません。下がったのは認知的複雑度だけです。SonarSourceのCognitive Complexity仕様は、if に1点を足し、ネストしていればその深さに応じた点をさらに足します。一方、早期の return そのものには点を足しません。ネストを浅くすれば、点が下がる仕組みです。
今回のように判定数を変えずにガード節へ書き換える場合、radonで測るサイクロマティック複雑度は下がらないため、CIのしきい値超過も解消しません。その場合は、判定の数そのものを減らす必要があります(辞書による対応表への置き換えや関数の分割など)。指標ごとの違いとツール別の既定値はサイクロマティック複雑度とは?計算方法・目安10の根拠とツールで値が違う理由で解説しています。
リンターで不要なelseを検出する設定と限界
「return の後の else は不要」という指摘は、主要なリンターが機械的に出せます。
| ツール | ルール | 既定 |
|---|---|---|
| ESLint | no-else-return | 設定で有効化 |
| Pylint | R1705 no-else-return | 有効 |
| Ruff | RET505 superfluous-else-return | 無効(extend-select で指定) |
ESLintのno-else-returnは allowElseIf オプションの既定が true で、return の後の else if は許されます。false にすると else if も指摘されます。PylintのR1705とRuffのRET505は同じ内容のPython版です。Ruff 0.17.2では、何も設定しない状態で有効になっている407ルールにRET505は入っていません。既定のルールを残したまま足すには、extend-select に指定します。
# pyproject.toml
[tool.ruff.lint]
extend-select = ["RET505"]
前の章の process_order_nested にかけると、結果はツールによって違いました。Ruff 0.17.2とPylint 3.3.9が指摘したのは、最も内側の else 1か所だけです。外側の2つは「if の本体が return で終わっていない」ため対象外でした。ESLint v10.13.0は、if を2段入れ子にしたJavaScriptの割引計算例で、内側と外側の両方の else を指摘しました。ただ、--fix で直した結果は else を消して中身を外へ出しただけです。if (user) { if (user.isActive) { ... } return 0; } return 0; という形でネストは残りました。
つまり、リンターがするのは不要な else の検出と、ESLintやRuffでの else の除去までです。条件を反転させてガード節に組み替える作業は、人が前の章の手順で行います。リンターは、書き換えた後に else が戻ってこないよう見張る役割と考えると使いどころを誤りません。
アーリーリターンを使うべきでない場面
アーリーリターンにも反対意見があります。根拠のある反論は次の3つに分けられ、それぞれに対処法があります。
単一出口原則が求められる関数
「関数の出口は1つにすべき」という単一出口原則(single exit)は、いまもコーディング規約に残っています。代表例はロッキード・マーティンが2005年に公開したJSF AV C++ Coding Standardsです。そのAV Rule 113は、MISRA Rule 82を改訂したもので、「Functions will have a single exit point.」と定めています。根拠は「出口が多い関数は理解も解析も難しくなりがち」というものです。
ただし、同じ規則には例外が書かれています。「単一出口にすると制御が分かりにくくなる、または大きく複雑になる(追加の変数を導入するなど)場合は不要」です。あわせて「後片付けはすべての出口で管理すること」とも書かれています。厳格な組込み向け規約でも、result 変数を持ち込んでネストを深くしてまで出口を1つにすることは求めていないわけです。
判断の基準は次のとおりです。短い関数で前提をチェックするだけなら、ガード節による複数の return を優先します。関数が長く、途中に副作用や後片付けが挟まるなら、出口を集約するか、関数を分割します。所属する現場の規約が単一出口を定めているなら、規約に従います。
後片付けを手書きする処理とgoto fail
波かっこのない条件分岐に無条件のジャンプが紛れ込んだ例が、2014年に見つかったAppleのSSL/TLS検証の脆弱性(CVE-2014-1266)です。Security-55471のsslKeyExchange.cでは、エラー時に後片付けへ飛ぶ goto fail; が2行続いていました。
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;
波かっこがないため、条件に従うのは1行目の goto fail; だけです。2行目は必ず実行され、err が0(成功)のまま後続の署名検証を飛ばしました。Appleは影響を「ネットワーク上の特権的な位置にいる攻撃者が、SSL/TLSで保護されたセッションのデータを取得・改変できる可能性」と説明しています。
これは return ではなく goto による途中脱出ですが、教訓はアーリーリターンにもそのまま当てはまります。1つ目は、ガード節を波かっこなしで次の行に書かないことです(1行に収めるか、波かっこで囲みます)。2つ目は、途中で抜ける関数では後片付けを手書きせず、with・try-with-resources・defer のような言語の仕組みに任せることです。
対等な場合分けへの乱用
アーリーリターンを避けたいという主張の多くは、「if の後に暗黙の else を読み手に補わせる」「if と後続の処理の間に別のコードを挟み込めてしまう」という点を問題にしています。この指摘は、本筋のない対等な分岐に return を使ったときに当たっています。
// 対等な3つの分岐。ガード節にする必要はない
function getLabel(score) {
if (score >= 80) {
return "A";
} else if (score >= 60) {
return "B";
} else {
return "C";
}
}
この3つの結末は、どれも正常な結果で、どれが本筋ということがありません。こうした分岐は if/else if/else や対応表で対称に書いた方が意図が伝わります。ESLintの no-else-return を既定の allowElseIf: true のまま使うと、else if は許されて最後の else だけが指摘されます。対称な書き方を優先する関数では、指摘に合わせて崩すのではなく、対応表への置き換えを検討します。
線引きは「関数に本筋の処理があり、その分岐が本筋から外れるケースを片付けているか」です。ファウラーの例でいえば、normalPayAmount() による支給額の計算が本筋で、isDead・isSeparated・isRetired に該当する場合は特別なケースです。こうした分岐はアーリーリターンにし、本筋のない対等な場合分けは if/else にします。ガード節を関数の冒頭にまとめ、本処理の途中から抜ける return を最小限にすれば、読み手が関数の終わり方を見失いません。条件式そのものを読みやすくするには、真偽を表す変数名の付け方が効きます。具体的な付け方は命名規則を英語で書く方法|ケース・品詞・対義語と公式ガイドの規定にまとめています。
よくある質問
アーリーリターンと早期リターンは違うものですか?
同じものです。「アーリーリターン」「早期リターン」「early return」「早期return」はいずれも、処理の途中で結果が確定した場合などに、return で関数から早く抜ける書き方を指します。
ガード節の読み方と、early returnとの違いは?
ガード節は「ガードせつ」と読み、英語ではguard clauseです。アーリーリターンが途中で抜ける書き方全般を指すのに対し、ガード節は、通常処理を続けない条件を先に処理して抜ける部分を指します。入力検証や特別な場合の処理に使います。
早期リターンを禁止しているコーディング規約はありますか?
JSF AV C++のAV Rule 113のように、単一出口を定める規約はあります。ただしその規約にも、単一出口にすると追加の変数などで制御が複雑になる場合は不要という例外があります。一方で、GoのCode Review Comments、CakePHPのコーディング規約(Bail Early)は早く抜けることを推奨しています。
Pythonでearly returnはどう書きますか?
関数の冒頭に if 条件: と return 値 を並べ、無効な入力を先に返します。ループの中では return の代わりに continue を使うと、その要素だけを飛ばせます。
early return patternとは何ですか?
ネストした条件分岐をガード節に置き換え、正常系を浅いインデントに残す定石のことです。ファウラーのカタログでは「Replace Nested Conditional with Guard Clauses」という名前で紹介されています。