この章では、Perlの例外処理について説明します。
コンピュータプログラミングにおいて、例外処理とは、プログラムの実行中に例外（特別な処理を必要とする異常な状態や例外的な状態）が発生した場合に対応する処理のことです。
例外が発生すると通常の実行フローが中断され、あらかじめ登録された例外ハンドラが実行されます。
この処理の詳細は、ハードウェア例外かソフトウェア例外か、またソフトウェア例外の実装方法によって異なります。
例外処理が提供される場合は、特殊なプログラミング言語構造、割り込みなどのハードウェア機構、またはシグナルなどのオペレーティングシステム（OS）プロセス間通信（IPC）機能によって支援されます。
Perlでは、例外処理は「特殊なプログラミング言語構造」を使用して行われます。
v5.34.0では、実験的なtry/catch 構文が追加されましたが、従来のeval {...}を使った方法を先に紹介します。
__TOC__
Perlの組み込み関数evalは、eval STRINGの形式に加えて、eval { ... }というコードブロックを取る形式があります。
eval { ... } のコードブロック内で、ゼロ除算のような組み込み例外やdie命令によって発生するユーザー由来の例外のいずれかが発生すると、evalは処理を中止し、$@に値を設定して返します。例外が発生しなかった場合、$@はundefとなります。
[https://paiza.io/projects/jEqH94n0krcG4pG0FE4M1A?language=perl eval{} と $@ を使った例外処理]:
use v5.30.0;
use warnings;
sub div {
    my ( $x, $y ) = @_;
    die "@{[join ':', caller]}: Domain error: div($x, $y)" if $x == 0 and $y == 0;
    $x / $y;
}
eval { div( 0, 0 ) };
warn $@ if $@;
eval { div( 1, 0 ) };
warn "@{[ join ':', (__PACKAGE__,__FILE__,__LINE__)]}: $@" if $@;
実行結果:
main:Main.pl:10: Domain error: div(0, 0) at Main.pl line 6. 
main:Main.pl:14: Illegal division by zero at Main.pl line 7.
6 行目で、分子分母ともゼロの除算をユーザープログラムがドメインエラーとして例外を die 関数を使って投げ、11 行目でハンドリングしています。
7 行目で、処理系がゼロ除算を上げ、14 行目でハンドリングしています。
このように例外を発火したり捕捉したコードは、組込み関数 caller を使って呼出し元のパッケージ名:ファイル名:行番号を表示すると、例外の原因と経路を調べる役にたちます。
/catch(変数){} 構文が追加されました。
実験的なので use feature qw(try);が必要で、警告を抑止するにはno warnings "experimental::try";が必要です。
「eval{} と $@ を使った例外処理」と等価なコードを示します。
[https://www.mycompiler.io/view/6Hrs5voKphX try{}catch{}を使った例外処理]:
use v5.34.0;
use warnings;
use feature qw(try);
no warnings "experimental::try";
sub div {
    my ( $x, $y ) = @_;
    die "@{[join ':', caller]}: Domain error: div($x, $y)" if $x == 0 and $y == 0;
    $x / $y;
}
try {
    div( 1, 0 )
}
catch ($e) {
    warn "At @{[__FILE__]} line @{[__LINE__]}: $e"
}
try {
    div( 0, 0 )
}
catch ($e) {
    warn "At @{[__FILE__]} line @{[__LINE__]}: $e"
}
実行結果:
At main.plx line 16: Illegal division by zero at main.plx line 9.
At main.plx line 23: main:main.plx:20: Domain error: div(0, 0) at main.plx line 8.
8 行目で、分子分母ともゼロの除算をユーザープログラムがドメインエラーとして例外を die 関数を使って投げ、15-17 行目でハンドリングしています。
9 行目で、処理系がゼロ除算を上げ、22-24 行目でハンドリングしています。
モダンな書き方ではありますが、「実験的」という性質上、公開するモジュールや実務での使用は、実験的な性質がなくなるまで控えた方が良いでしょう。
v5.36.0 で、try{}catch(変数){}構文にfinally{}節が追加されましたが、やはり実験的な機能です。
[try{}catch(変数){}finallyを使った例外処理]:
use v5.36.0;
use warnings;
use feature qw(try);
no warnings "experimental::try";
sub div {
    my ( $x, $y ) = @_;
    die "@{[join ':', caller]}: Domain error: div($x, $y)" if $x == 0 and $y == 0;
    $x / $y;
}
foreach my $i ( 0, 1 ) {
    foreach my $j ( 0, 1 ) {
        try {
            say "div($i, $j) --> @{[ div( $i, $j ) ]}"
        }
        catch ($e) {
            warn "At @{[__FILE__]} line @{[__LINE__]}: $e";
            next
        }
        finally {
            say "finally! \$i = $i, \$j = $j"
        }
        say "plain. \$i = $i, \$j = $j"
    }
}
実行結果:
At finally.pl line 18: main:finally.pl:15: Domain error: div(0, 0) at finally.pl line 8.
finally! $i = 0, $j = 0
div(0, 1) --> 0
finally! $i = 0, $j = 1
plain. $i = 0, $j = 1
At finally.pl line 18: Illegal division by zero at finally.pl line 9.
finally! $i = 1, $j = 0
div(1, 1) --> 1
finally! $i = 1, $j = 1
plain. $i = 1, $j = 1
前節のプログラムと基本的に同じですが、分子分母をループで回しました。
例外をcatchしたときは next でループの内側の先頭に戻っています。
next の影響で say "plain. \$i = $i, \$j = $j"は例外が出ると実行されません。
しかし、finallyコードブロックのsay "finally! \$i = $i, \$j = $j"は、例外の発生有無にかかわらず実行されます。
finallyコードブロックは、例外の有無にかかわらず必ず実行する処理（例えばファイルハンドラのクローズなど）を想定していますが、同様のことはPerlのv5.36.0から導入されたdeferコードブロックでも実現できます。
deferコードブロックは、スコープが終了する時点で必ず実行され、LIFO順で実行されることが保証されています。また、deferブロック内で例外が発生した場合、それは通常の例外と同様に処理されます。
[defer{}try{}catch(変数){}を使った例外処理]:
use v5.36.0;
use warnings;
use feature qw(try);
no warnings "experimental::try";
sub div {
    my ( $x, $y ) = @_;
    die "@{[join ':', caller]}: Domain error: div($x, $y)" if $x == 0 and $y == 0;
    $x / $y;
}
foreach my $i ( 0, 1 ) {
    foreach my $j ( 0, 1 ) {
        use feature 'defer';
        defer {
            say "defer! \$i = $i, \$j = $j"
        }
        try {
            say "div($i, $j) --> @{[ div( $i, $j ) ]}"
        }
        catch ($e) {
            warn "At @{[__FILE__]} line @{[__LINE__]}: $e";
            next
        }
        say "plain. \$i = $i, \$j = $j"
    }
}
実行結果:
At defer.pl line 22: main:defer.pl:19: Domain error: div(0, 0) at defer.pl line 8.
defer! $i = 0, $j = 0
div(0, 1) --> 0
plain. $i = 0, $j = 1
defer! $i = 0, $j = 1
At defer.pl line 22: Illegal division by zero at defer.pl line 9.
defer! $i = 1, $j = 0
div(1, 1) --> 1
plain. $i = 1, $j = 1
defer! $i = 1, $j = 1
finally{}は、defer{}よりも例外処理の意識度が高いという意味で、コードの可読性を高めるための構文糖と言えます。
また、defer{}は、try/catchだけでなく、eval{};if($@){...}と組み合わせることもできます。
Carpモジュールのcroak関数は、現在のサブルーチン（またはメソッド）を呼び出した場所を含めたスタックトレースと共に、エラーメッセージを表示してプログラムを終了します。
これにより、開発者は問題のある箇所を特定し、デバッグを行うことができます。
モジュールの場合、croak関数を使って例外をスローすることができます。
この場合、エラーメッセージにはモジュールの名前が含まれますが、例外が発生した場所はモジュール内部ではなく、モジュールを使用しているスクリプトファイルです。
以下は、例外がモジュールを超えてスローされる例です。
MyModule.pm:
package MyModule;
use Carp qw(croak);
sub do_something {
    my ($x, $y) = @_;
    croak "Domain error" if $x == 0 and $y == 0;
    croak "Division by zero" if $y == 0;
    return $x / $y;
}
1;
main.pl:
use MyModule;
my $result = eval { MyModule::do_something(10, 0) };
warn "An exception occurred: $@" if $@;
この例では、main.plスクリプトファイルでevalブロックを使って、MyModule::do_something関数を呼び出します。
この関数は、引数$xと$yがともにゼロの場合にcroak関数を使って例外（"Domain error"）をスローします。
また、引数$yがゼロの場合にもcroak関数を使って例外（"Division by zero"）をスローします。
evalブロックは、この例外をキャッチして警告メッセージを表示します。
このように、croak関数を使うことで、モジュールを超えた例外処理が行えます。