例外処理の使い所と使い方 【プログラミング発展】
例外とは、設計上想定されていない入力や処理が行われたときに問題を処理するためのものです。
もし例外処理をしなかったら
例えばファイルの読み込みを考えてみましょう。例外処理をしなかった場合を考えてみます。
const fs = require('fs'); const fileName = "file.txt"; // ファイルがあることが保証されているとして読み込み body = fs.readFileSync(fileName, "utf8"); console.log("ファイルの読み込み処理終了"); // 色々な処理...
このとき、ほとんどの場合は正常に処理されます。
しかし、もしファイルを読み込めなかった場合はどうでしょう。ファイルがあるなら大丈夫だろうと思いがちですが、ストレージがエラーを吐いた、他のプログラムがちょうど書き換え処理を行っていて読めなかったなど、様々な理由で思わぬ失敗がありえます。
さて、そのようにして読み込みに失敗したらどうなるでしょうか。エラーを吐いてプログラムは落ちます。そうなると、データベースのデータが中途半端だったり、ユーザからすると何の表示もなく失敗しているなど、大問題です。
例外処理の書き方
try-catch
を用いて例外排出時の処理を書くことで、失敗したときにどうするかを指定できます。
const fs = require('fs'); const fileName = "file.txt"; let body = null; try { body = fs.readFileSync(fileName, "utf8"); } catch (e) { console.error(e); // その他、失敗時の処理 // ... } // 色々な処理 console.log("ファイルの読み込み処理終了");
言語によって書き方に細かい差がありますので、使いたい言語の書き方を確認してください。
例えば、pythonはtry-except
です。
try
とcatch
が登場しました。例外を排出する可能性がある部分をtry
で囲み、例外が出たときの処理をcatch
に書きます。
例外が排出されると、その時点でtry
内の処理は中断され、catch
内の処理を開始します。その処理が終われば、try-catch
後の処理が実行されます。
ここで、例外を排出する行為を、例外をthrow
する (投げる) と表現したりします。そして、throw
した (投げた) ものはcatch
します。もちろん、throw
しなければcatch
しません。
必ず実行するものを書くには
例外を排出したしていないに関わらず処理するものは、finally
に書きます。
try { // ... } catch (e) { // ... } finally { // try-catchを終えたら必ず実行される処理 }
例外の排出方法
上の例では、ライブラリ内でthrow
されているものを外でcatch
しているパターンです。次は、自分から例外をthrow
します。
try { // 処理... console.log("throw前"); throw "例外をthrowしました"; console.log("throw後"); } catch (e) { console.error(e); }
throw前 例外をthrowしました
e
はerror
の頭文字です。しばしばこの引数名が使われます。
このように、throw メッセージ
とすることで例外を排出できます。もちろん、throw
するとそれ以降のtry
内の処理は実行されません。
また、return等と同様にthrow文があっても実際にthrowされなければ普通に処理されます。普通に処理されれば、catch
内の処理は実行されません。
try { const x = 0; console.log("前"); if (x) { throw "err"; } console.log("後"); } catch (e) { console.error(e); }
前 後
別の関数内で例外を出す
メソッド内の処理で例外を排出したもののcatch
がない場合、外側の関数でcatch
できます。
function f() { throw "例外排出"; return 100; // ここは実行されない } let x = 0; try { x = f(); } catch (e) { console.error(e); } console.log(x);
例外排出 0
関数内でthrow
していますが、その場所にはcatch
がありません。この場合、関数の呼び出し元でcatch
します。もしそこもcatch
がなければ、更に外でcatch
します。
function f() { throw "err"; } function f2() { return f(); // fで例外を排出しているがcatchしていないため, 更に外でcatchする } try { f2(); // fの例外がここでようやくcatchされる } catch (e) { console.error(e); }
err
また、途中でcatch
されれば、関数の呼び出し元ではcatch
されません。
function f() { throw "err"; } // ここでcatch function f2() { try { f(); } catch(e) { return null; } } try { f2(); // fで例外がでるが、f2でcatchしているのでここではcatchされない console.log("続き"); } catch (e) { console.error(e); }
続き
この場合でも、catch
後に再度throw
すれば、外側の関数でcatchできます。
tryの範囲は狭くするべきか
一般的に、try
の範囲は狭くしたほうが良いと言われています。
- 例外を排出する処理としない処理を明確にするため
- どの例外が起こったらどの処理をするかを明確にするため
例えば、次の処理を見てみましょう。
try { // DBの読み込み処理 (例外あり) if (isExistXx) { // 変数上の一部の値を更新 (例外なし) // DBのデータを更新 (例外あり) } else { // DBデータ作成 (例外なし) // データ追加 (例外あり) } // 別のデータの読み込み処理 if (/* ... */) { ... } } catch (e instanceof ReadError) { console.error(e); } catch (e instanceof WriteError) { // ... } catch (e instanceof UpdateError) { // ... } catch (e) { // その他のエラー }
この処理では、複数の例外を出す可能性がある処理を、1つのtry
に詰め込んでしまったがために問題が出ているパターンです。
2つある読み込みの片方で例外を出したとして、どちらの処理で例外が出たかでうまく処理を分けられるでしょうか?
1個目のデータ更新で失敗したとして、2つ目のデータは完全無視で良いのでしょうか?
仕様変更をする際に大変な改修が必要になりますね。テスト範囲も増大します。
let isExistXx = null; try { // 読み込み } catch (e) { // ... } if (isExistXx) { // 変数上の一部の値を更新 try { // DB更新 } catch (e) { // ... } } // ...
と分けることで、どの処理でどの例外が発生する可能性があり、どのタイミングの例外でどう処理するのかを明確に切り分けられます。
2つ目の例でも不吉な臭いがあるのではというツッコミはなしでお願いします。
使い所
よくある間違いは、エラーが出たときの分岐に利用する行為です。ただの分岐であれば、普通のif文を利用するべきです。
使い所は、基本的にめったに起こらないような、異常事態であるエラーに対処するためのものです。 IOエラー、通信エラー、予期しないデータの不整合などがそれに当たります。
まとめ
例外排出を利用することで、想定しないエラーが発生しても動作を止めずに (要はシステム障害を起こさずに) 処理を続けることができます。
if文やgotoのようにも動かせますが、用途が異なります。適切なケースで利用していきましょう。