第6章 フェイルセーフ
エラーハンドリング

エラーハンドリングの重要性

ソフトウェアの中で発生するエラーは多岐にわたる。エラーには、代替策をとれば問題なく先へ進めるものや、プログラム自身が問題で修復できるものもある一方で、別のコンピュータへ役割を引き継いだり、人間の介入を待たなくてはならないものまで多種多様である。発生の「深さ」も、デバイスドライバ、オペレーティングシステム、データベース、ミドルウェア、ビジネスロジック、ユーザインタフェース等のあらゆる「階層」にわたる。

これらのエラーに適切に対処すること(エラーハンドリング)は非常に重要である。発生を見過ごしていたり対処が不適切であると、プログラムは想定外の振る舞いをしかねない。これらは悪くすると、機械類の重大な誤作動、システムのサービス不能、データ破壊、情報漏えい等の結果を生む。

エラーハンドリングはプログラムに占める比率も大きい

エラーハンドリングは、ソースコードの中で大きな比率を占める存在である。

例えば、下位のモジュール(関数)を利用した次の図のような3ステップからなる処理を行う必要があるとする。

basic steps
図6-2: 3つのステップからなる処理の例

各ステップはどれも失敗するおそれがある。そのため、プログラマが記述するロジックは、実際には各ステップの失敗を考慮したものになる。(下図)

considering errors
図6-3: 失敗への対処が必要

このようにして、プログラムの中は多くのエラーハンドリングのコードで占められてゆく。

エラーの想定

ソフトウェアを構築する際には、起こりうるエラーをひととおり想定し、それらに対処するコードを書く必要がある。

エラーの想定は、要件定義から詳細設計に至る工程のどれにおいても行う必要がある。システムの機能性、構造、仕様を決めてゆく過程で、そこで起こりうるエラーも段階的に明らかになってくるからである。

その際、エラーへの対処方法を立案するのであるが、対処方法は概ね次の9つのパターンに分類できる(詳しくは後述)。

  1. 「スキップ」
  2. 「デフォルト値」
  3. 「別ロジック」
  4. 「入力の再要求」
  5. 「同じ処理の再試行」
  6. 「自己プログラムの再起動、OSの再起動」
  7. 「上位モジュールへの失敗の通知」
  8. 「他マシンへの役割の引継ぎ」
  9. 「プログラムの停止、マシンのシャットダウン」

エラーハンドリングの内容

エラーハンドリングには、一連の流れ(検出→伝達→対処)がある。

error handling
図6-4: 検出・伝達・対処

 

検出:

対処すべきエラーは、その発生が的確に検出されなくてはならない。

古典的なC言語用ライブラリにおいては、関数の戻り値でエラーを表していた。関数の戻り値をチェックしないコーディングがしばしば行われたため、エラーの発生が見逃されがちであるという問題があった。

C++においては「例外(exception)」が導入された。ランタイムライブラリ等はエラーを例外の発生によって通知するようになり、プログラマは例外の発生を無視できなくなった。

例外を捕捉する際に注意すべきことは、明らかに発生が想定できるもの以外は捕捉しないということである。プログラムの実行が強制終了させられるのを避けるつもりで次のようなコードを書いてしまうと、すべての例外がマスクされ、本来対処すべきエラーの発生に気づかないおそれがある。(catch(...) はすべての例外を捉える際のC++の記法である。)

	try {
	   (例外を発生させるかもしれない処理)
	} catch (...) {
	   // 何もしない
	}
    
悪い例

なお、C++のデストラクタの中ではこのような記述は必要になる。

伝達:

検出されたエラーは、その事象への対処を担当するモジュールへ伝達されねばならない。伝達の手段には、関数の戻り値、関数が書き込む構造体のフィールド、C++の例外等がありうる。

対処:

エラーの対処方法は多種多様である。エラーへの対処のバリエーションには、概ね下記の9つのパターンが考えられる。:

  1. 「スキップ」──失敗した処理をスキップして先へ進んでよい状況。ただちに対策を施さなくても、のちに検証・修復を実施したい状況においては記録を残す。記録については以下のパターンも同様。
  2. 「デフォルト値」──情報の取得・生成に失敗しても、省略時解釈の余地がある状況。
  3. 「別ロジック」──ロジックAが失敗したらそれとは異なるロジックBを試みる余地のある状況。
  4. 「入力の再要求」──妥当でない入力パラメータに関し、ユーザに値の入力を再度求めたり、通信相手に再送信を求めることができる状況。ただし、一定の閾値でタイムアウトする必要がある。タイムアウトした場合は、反復全体がエラーであるとしてさらなる対処を行う。
  5. 「同じ処理の再試行」──無線通信等、処理の成功に不確実さがある状況。ただし、一定の閾値でタイムアウトする必要がある。タイムアウトした場合は、反復全体がエラーであるとしてさらなる対処を行う。
  6. 「自己プログラムの再起動、OSの再起動」──そのマシンにおける稼動の継続が見込める状況。
  7. 「上位モジュールへの失敗の通知」──当該モジュールでは意味のある対処はできないが、責任を委ねる先の上位プログラムが存在する状況。
  8. 「他マシンへの役割の引継ぎ」──当該マシンにおける稼動の継続は困難であるが、バックアップマシンが存在する状況。
  9. 「プログラムの停止、マシンのシャットダウン」──自ら行えることが何もない状況。

リソース解放:

エラーへの対処とともに行うべき重要なことに、リソースの解放がある。

プログラムは必要に応じて、ヒープメモリのブロック、データベース接続、ファイルディスクリプタ、プールされたスレッド等の各種リソースを動的に割り当てて利用する。エラーによる処理の中断ののちもこれらを解放せずにおくと、やがて使えるリソースが枯渇し、プログラムの実行を継続できなくなる。いわゆるメモリリークやリソースリークと呼ばれる現象である。

エラーへの対処時に行うリソース解放は、本稿冒頭の3ステップの処理について言うと、例えば、次のような形になる。

structure
図6-5: 処理中断時にリソースを解放する

エラーハンドリングを実装する際の考慮事項

例外の捕捉は最小範囲で:

try - catch 構文による例外の捕捉と対処は、次のように最小範囲に絞ることが望ましい。

なぜならば、プログラムの当該地点で想定しているもの以外についても例外を吸収し他へ伝えずにいると、それらへ対処する仕組みをもつかもしれない上位モジュールにエラー発生が伝わらないからである。

C++でデストラクタを記述する際の注意事項:

C++においては、プログラムの制御が関数から出る際、自動でデストラクタが呼ばれるメカニズムを利用して、リソース解放を行うことができる。ヒープ上に置かれたオブジェクトに関しても、このオブジェクトへの参照を保持するオブジェクトをスタック上に設けることによって同様のことが行える。

ただし、注意すべきことは、デストラクタ内のリソース解放が例外の発生によって中断されてはならない点である。デストラクタの中では、記述するリソース解放処理の各ステップを、例えば、次のような要領でガードする必要がある。

 	try {
	   (リソース解放のステップ1)
	} catch (...) { /*必要ならログ等へ障害を記録*/ }
	try {
	   (リソース解放のステップ2)
	} catch (...) { /*必要ならログ等へ障害を記録*/ }
	
	  〜
	try {
	   (リソース解放のステップn)
	} catch (...) { /*必要ならログ等へ障害を記録*/ }

デストラクタにおいてガードする例

 

C言語におけるエラー処理のヒント:

例外処理の仕組みを提供するOSもあるが、言語仕様の上ではC言語に例外処理機構はない。例外処理機構が備わっていないか利用しないことを選択した場合、例えば、次のような関数呼び出しの構造を設けることが考えられる。

それは、業務分野の処理を専門に行う関数を、リソースの割り当て・解放を専門に行う関数が囲い込む形に関数の呼び出し構造を作るというものである。内側の関数ではエラーによる処理の中断が起こりうるが、それらを外側の関数で受け止めてリソース解放を行う。

double layered structure
図6-6: 処理中断とリソース解放の二重構造

C、C++およびこれらから影響を受けているいくつかのプログラミング言語について、例外処理とリソース解放のための構文およびランタイム機能の対比を、次ページの表に示す。