プログラミング初心者の内は特にtry-catchは使いどころを間違えやすいですし、throw句で何故あえてクラッシュさせるの?とかも思いますよね。
また、熟練者に差し掛かっても、これら論点の運用はなかなか腹落ちしないものです。
私自身、10年以上独習でプログラミングを続けてきて、経験の中で答えが何となく見えてきましたので、共有してみたいと思います。
◇目次
【総論】設計ミスや想定外に対しての例外スローは、損害を最小限に食い止める安全装置
始めに結論をまとめます。尚、本記事は設計思想的な面を含みますので、絶対的な答えではないことはご注意ください。また、今回はコード例ではC#を使用しますが、大半の言語で通用する話です。
- 例外は、設計ミスや想定外の入力等に対する影響を早期検知する安全装置。ビジネス上の損害範囲拡大を食い止めるため、あえてスローされる必要がある。
- コーディング規約やクラッシュした時の影響は事前に確認する必要がある。(誰に向かって例外を投げているのか?)
- try-catch句は、上記のような特定の目的をもって使用されるべき。初心者の内は特に不適切に使ってしまわないよう要注意。
例外スローの運用は、ビジネス上の危機管理に直接的に紐づく責任論点
リリースするプログラムでは様々なケースのテストを行うのは当然ですが、それでもミスを埋め込んでしまう可能性はゼロにはできません。あるいはメソッドに想定外の入力があり、正常に処理を続行できないケースは生じ得ます。
そういった状況に起因して誤計算や責務不履行を起こし、しかも例外を投げずに表面上は正常(内部的には異常)に動いていたとしたら、プログラムの種類、例えば業務システムなどでは後々大きな責任問題を引き起こす可能性すらあります。
ビジネス上の問題に発展する懸念に比べれば、その場で例外を投げてプログラムをクラッシュさせてしまう方が遥かにマシである場合が多い(「トラブル時は安全側に倒す」危機管理対策の履行)ため、問題が起きたらきちんと例外をスローのは実装側の責務といえます。(もちろん、当然のことながらプログラムの種類などにより個別検討は必要です。)
例外のスローは安全装置であり、開発チームに対する優しさであり、実装者の責務である
このように、例外のスローはバグを即座に顕在化させ、ビジネス上の損害を最小限に食い止める効果があります。
また、開発やテストフェーズにおいてもバグの取りこぼしの可能性を低めることが可能です。
開発チームメンバーが犯すミスが予見されれば、そこに例外処理をきちんと埋め込んでおくのは優しさ・思いやりであり実装者の責務とも言えます。
例外スローの具体例を考える
ごく簡単な例で考えてみます。
設計ミスに対する例外スロー
var list = GetList();
if (list == null || list.Count != 1) {
// 例外をスロー
throw new Exception("リストの構成が想定外です!");
}
//以下、正常処理
このケースでは、GetListメソッドから返却されたリストコンテンツが想定通りか否かを検証しています。(例えばSQLでSELECT文の結果を得るケースなどでしょうか。)
一意の列に対してサーチしているのに結果が2行以上存在しているなどの不具合は、致命的な計算ミスに繋がりかねません。よって、その場で例外をスローして処理を中断しています。
想定外の入力(引数)に対する例外スロー
//文字列から日付を読み取る。"yyyy/MM/dd"という入力を想定
int GetDateFromText(string ymd) {
var splitedValue = ymd.Split('/');
if (splitedValue.Length != 3) {
// 例外をスロー
throw new ArgumentException("入力値はyyyy/MM/dd形式である必要があります。");
}
//正常処理
}
入力値が正しくない場合に例外スローを行っています。
例外を使用せずbool値やエラーメッセージを返り値とする手法も考えられますが、変数やif分岐が増えてコードが無用に煩雑になりがちです。また、意味としても「正常でないなら例外を投げる」といった挙動のほうが本来的な例外運用としても正しいと言えます。
このように、シンプルに例外にメッセージを投げてしまう設計の方が可読性・工数の両面でメリットがあると言えます。
例外スロー時は、余計な情報(ソースコード等)が表に出ないように。コーディング規約も確認。
設定と環境によっては、例外が発生した際、UI上にソースコードやスタックトレースが丸見えになってしまう場合があります。品質やセキュリティの観点で好ましくない現象です。
ロジック内からの例外スローを許可する場合は、上位層でどう処理されるかをキチンと把握する必要があります。
また、チームで開発している場合は当然ながらプロマネ等に確認を取り方針を確認するようにしましょう。
あなたのコードは誰に向かって例外を投げているのか?を考える
例外を投げることは優しさである、と伝えましたが、投げた例外をキャッチ(適切に捕捉)する機構がなければ意味を成しません。
コードの深層(スタック内=呼び出し先)であなたのコードが投げた例外がどのような顛末をたどるのか?上述のようにUIにスタックトレースを晒す結果にならないか?コードのエラー検知にきちんと役立ててもらえるか?を意識しておくのがよいでしょう。
例外をキャッチするtry-catch句の使いどころは?
以上のように、例外は必要に応じて投げる必要があることがわかりました。一方、try-catch句で例外をキャッチ(捕捉)する方が良いケースを検討します。
必要なエラーが隠蔽されてしまう可能性があるので無闇な利用は避けるべきですが、例えば以下のようなケースでは逆にtry-catch句を使用することが必要と思われます。
いずれにしても、使用する目的をはっきりさせた上で組み込むことが望ましいと言えます。
呼び出し元(上層)でまとめて例外を捕捉する場合
メソッド内でスローされた例外は、呼び出し元に連鎖して伝播し、最上位層で捕捉されなければプログラムがクラッシュします。
上述の通り、実際にクラッシュさせると利用者に余計な情報を与えてしまい、脆弱性や情報流出に繋がるケースも考えられます。これを防ぐため、例えば処理のエントリポイントでまとめてキャッチするのがよいでしょう。
try {
RunApp();
} catch (Exception e) {
//ロギング
Logging(e.Message, e.StackTrace);
//エラービューを表示
return ErrorView();
}
アンマネージドリソースの処理(finally句の利用)
多くの言語では、データベース接続などの外部リソース(アンマネージドリソース)は、例外が発生した場合はその場での追加処理(接続を閉じる、ロールバックする等)が必要となります。
このようなケースでは、一旦try-catchで例外を捕捉します。その後、例外を隠蔽しないようにthrowし直すのが通常のケースでしょう。
尚、C#におけるusingステートメントなど、このようなリソース管理を自動化してくれる機能がある場合が多いです。そういったものの利用も検討しましょう。
DbConnection connection = new DbConnection();
DbTransaction transaction; = new DbTransaction();
try {
//DB接続・トランザクション開始
connection.Open();
transaction = connection.BeginTransaction();
//正常処理
transaction.Commit();
} catch {
transaction?.Rollback();
} finally {
connection.Close();
connection.Dispose();
transaction.Dispose();
}
外部APIで例外制御が不能な場合
ライブラリなどで望ましくないタイミングで例外を投げてくるケースで、処理層側で捕捉したい場合などは想定され得ます。
私が実際に出会ったケースでは、非同期のWebAPIの呼び出しで、レスポンスを待たず処理を終了させることがライブラリで対応されておらずタイムアウトで例外が投げられるケースがありました。
このケースにおいては、処理層側で呼び出しメソッドをtry-catchでかこう対処をしていました。
NGなtry-catchの書き方
逆に、try-catchのNG例を考えてみます。初心者あるあるな事例が多めです。
検証用途で使ってしまう
try-catchは例外のキャッチを行う構文ですので、その他の検証用途で使用するのは基本的には避けるべきです。このような使い方で上記のように必要なエラーを隠蔽してしまわないように注意しましょう。
極端な例になりますが、try-catchを使用すべきでない場面だけ例示してみます。
C#
// NGコード
// (listの要素数検証はtry-catchで処理せず、極力事前検証するべき)
public int? GetFirst(List<int> list)
try {
return list[0];
} catch {
Console.Write("エラー:リストが空だよ");
return null;
}
}
//修正後のコード(リストが空なら例外がスローされる)
public int? GetFirst(List<int> list)
// 事前検証
if (list.Count == 0) {
return null;
}
return list[0];
}
処理層側で全体をtry句にネストしてしまう
プロマネがクラッシュは絶対ダメと言ってるから、自分が実装する処理全体をtryに入れてしまえばいいんだ!というもの。
気持ちはわかります。
ただ、不具合の理由が分からなくなりますし、しかも想定外の例外までキャッチしてしまうため、かなり好ましくありません。内部で例外が出るようであれば、無闇にキャッチせずにコードを修正するべきです。
void MyApi() {
try {
//実装
//・
//・
//・
} catch {
//ログに出力
}
}
ループ内でtry-catchを使用する
try-catch句が作動すると、スタックトレース生成をはじめ相応の処理コストがかかりがちです。
よってループ内にtry-catch(相応に例外が発生するもの)を組み込んでしまうと、処理速度のボトルネックを生む可能性が高くなります。
結び:例外とtry-catchの運用は、成果物の品質、ビジネス危機管理に直結する重要論点
改めて、品質に直結する論点だと思いますので、特に責任者の立場にある方はよく検討した上で方向性を決めるべき議題です。
是非この記事もご参考いただいた上、よりよいプロジェクトにしていただければと思います。
記事筆者へのお問い合わせ、仕事のご依頼
当社では、IT活用をはじめ、業務効率化やM&A、管理会計など幅広い分野でコンサルティング事業・IT開発事業を行っております。
この記事をご覧になり、もし相談してみたい点などがあれば、ぜひ問い合わせフォームまでご連絡ください。
皆様のご投稿をお待ちしております。