【PHP】try と finally それぞれの中で例外が投げられると finally 中の例外だけが残る

  • 2021年12月22日
  • 2021年12月22日
  • PHP

 題の通りです。恐らくPHP以外の例外持ち言語のほとんどでも同様です。

 PHP では try, catch, finally という例外を処理する仕組みがあります。
PHP: 例外(exceptions) – Manual
 これは次の様な処理の分岐を作る PHP 組み込みの機能です。

try {
  // 例外が発生しうる処理
} catch (例外クラス名 $e) {// PHP8以降は変数名を省略可能
  // 例外についての処理
} finally {
  // 例外が発生してもしなくても行われる処理
}

 この機能は主にこれ以上処理を続行できない状況で処理を打ち切り、打ち切った理由を返すために用いられます。例外を用いない場合はおおよそ次の様に関数やメソッドの返り値にエラーパターンを含めて適切な処理を呼び出し側に求めます。この方法は例外を持たない言語(Go, Rustなど)でしばしば見られます。

/**
 * 例外を用いず処理する関数。 try 相当 
 * @return array [成功結果, 失敗結果]
 */
function notException(): array {
    // 処理A本体がここに入ります
    if(!処理A成功){
        return [null, 処理Aについてのエラー内容];
    }

    // 処理B本体がここに入ります
    if(!処理B成功){
        return [null, 処理Bについてのエラー内容];
    }

    return [処理A処理Bを成功して得られた結果, null];
}
[$result, $err] = notException();
if($err){
    // エラーハンドリング。catch 相当の処理
}
// 処理の成否関係なく実行される処理。 finally 相当の処理

 メモリーエラーの様な返り値も何もあったものじゃないエラーの場合は標準出力等にプログラムがいい感じにエラー内容を出力してプログラムを終了するのが基本です。

 総じて例外ありの方が短く記述できます。一方で例外なしの方が処理順が明確で、エラーが返ってくる場合がある処理とない処理が明確にわかります。一長一短です。例外の場合、最後まで処理されない投げっぱなし例外についての問題もありますが大体の言語はいい感じにユーザーに表示される最終処理場を言語組み込みで用意してくれています。
 余談ですが Java が例外を使いつつ関数等にどんな例外が投げられる場合があるかを明示させることを求める言語であったり、Goがreturn済みでも実行される処理を定義するための defer を持つ言語であったりと、上の長所短所は大概あいまいで個々の言語によるところが大きいです。

 この例外処理ですが、投げられている例外は常に一つというのが暗黙のうちに定まっています。なぜそうなっているかと、それが重要になる状況を説明します。コード例としては次です。

<?php

try {
    try {
        throw new \Exception("in try exception");
    } finally {
        throw new \Exception("in finally exception");
    }
} catch(\Exception $e) {
    echo $e->getMessage()."\n";
}

echo "この echo は例外と無関係な処理です。";

 try で例外が発生し、例外の発生の有無に関わらず実行される finally 内でも例外が発生します。この状況において外側の catch は二つの例外を処理対象にする様にも見えます。しかし二つとも処理すると仮定するとなかなか不穏な状況が引き起こされます。例えば、その後の処理の多重化、catch の中で回避が難しい計算量O(n)の処理が発生、多重で例外とcatchの組み合わせが起きると計算量O(m^n)の世界に入りだす、といった具合です。予想ですが、このあたりが理由で投げられている例外は常に一つとなる言語が多いのではないでしょうか。
 そんなわけで PHP で上の様なコードを実行すると次の様になります。

<?php

try {
    echo "1 try\n";
    try {
        echo "2 try\n";
        // 必ず try 中で例外が投げられる
        throw new \Exception("in try exception");
    } catch(\Exception $e) {
        echo "3 catch\n";
        echo "  ". $e->getMessage()."\n";
        throw $e;// ここで try 由来の例外を更に上に投げる
    } finally {
        echo "4 finally\n";
        // finally 中でも例外を投げる
        throw new \Exception("in finally exception");
    }
} catch(\Exception $e) {
    // ↑の投げられた例外をキャッチ
    echo "5 catch\n";
    echo "  ". $e->getMessage()."\n";
} finally {
    echo "6 finally\n";
}
/*
1 try
2 try
3 catch
  in try exception
4 finally
5 catch
  in finally exception
6 finally
 */

Online PHP editor | output for LoILl#missing exception
 try で投げられた例外が消えました。内側の catch を消しても finally 側の例外だけが捉えられて try 側は消えます。公式ドキュメントにある finally 中の return 文と同様の処理な感じです(下リンクの catch についての説明の末尾がそれです)。
PHP: 例外(exceptions) – Manual#catch

 ちなみに恐らくこの例外の処理優先の仕組みは多くの言語で共通です。少なくとも JavaScript, Python, Java は次のコードで PHP 同様に try 側が消えて finally 側が優先されました。

try {
  console.log('1 try');
  try {
    console.log('2 try');
    // 必ず try 中で例外が投げられる
    throw new Error("in try exception");
  } catch($e) {
    console.log('3 catch');
    console.log($e.toString());
    throw $e;// ここで try 由来の例外を更に上に投げる
  } finally {
    console.log('4 finally');
    // finally 中でも例外を投げる
    throw new Error("in finally exception");
  }
} catch($e) {
  // ↑の投げられた例外をキャッチ
  console.log('5 catch');
  console.log($e.toString());
} finally {
  console.log('6 finally');
}
try:
    print(1)
    try:
        print(2)
        a = 1 / 0
    except ZeroDivisionError as e:
        print(3)
        raise e
    finally:
        print(4)
        a = 1 / 'a' 
except ZeroDivisionError as e:
    print(5)
except TypeError as e:
    print(6)
finally:
    print(7)
import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        try {
        	System.out.println(1);
            try {
            	System.out.println(2);
    		    throw new Exception("in try");
            } catch (Exception e) {
            	System.out.println(3);
            	System.out.println("    "+e);
            	throw e;
            } finally {
                System.out.println(4);
    		    throw new Exception("in finally");
            }
        } catch (Exception e) {
        	System.out.println(5);
        	System.out.println("    "+e);
        } finally {
         	System.out.println(6);
        }
    }
}
>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG