例外処理の使い所と使い方 【プログラミング発展】

例外とは、設計上想定されていない入力や処理が行われたときに問題を処理するためのものです。

もし例外処理をしなかったら

例えばファイルの読み込みを考えてみましょう。例外処理をしなかった場合を考えてみます。

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です。

trycatchが登場しました。例外を排出する可能性がある部分を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しました

eerrorの頭文字です。しばしばこの引数名が使われます。

このように、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のようにも動かせますが、用途が異なります。適切なケースで利用していきましょう。

try catch

役に立ったらシェアしよう!