【プログラミングで必須】エラー処理の、大事な3つの考え方

error-handling

エラー処理。プログラミングの学習中は、なかなか関心が持たれない。だけど実際には、とても大事な必須の処理。

エラー処理は、どのような考え方で作るべきなのでしょうか?

目次

エラー処理ではなにをするのか?

想定外の事象に対処する

プログラムでは、動作の途中でしばしば、普段では起こらない事象に遭遇します。

例えばお買い物サイトプログラムなら

お買い物サイトプログラムの異常事態
  • 商品購入しようとおもったら、ちょうど売り切れになっていた…
  • 購入決算のためにサーバに接続しようとしたら、つながらない
  • なぜか分からないが、突然プログラムが途中で終わった

このような、通常の動作と異なる挙動に対処するのが、エラー処理です。

エラー処理がなければ、

  • 売り切れになってるのに、買い物手続きが進んでしまう
  • 途中で処理中断したのに、金額清算されてしまう

といった事態が起こります。

これを防ぐために、エラー処理が必須なわけです。

エラー処理の、大事な3つの考え方

エラー処理のプログラミングで重要なのは、以下の3つの考え方。

エラー処理の大事な3つの考え方
  • エラーが発生したら、すみやかに中断する
  • エラーが発生した時点で、リソースを確実に開放する
  • 状況分析のため、エラー情報を記録する

大事なこと(1)すみやかに中断する

プログラムは、エラーが発生したら、正常の処理を中断して、異常時の処置に移る必要があります。

エラーが発生した時点で、即時に中断する。大切です。

エラー状態のまま処理を進めてはいけません。商品が売り切れになったのに決済画面に進むのは、おかしいですよね。

「トラッシュよりクラッシュ」という言葉があります。エラーのまま混迷な状態に陥るよりは、その場で停止したほうがまだマシだ、という意味です。

エラーを無視せず、すみやかに対処しましょう。

大事なこと(2)リソースを確実に開放する

リソースとは、プログラムが使うなにかしらの材料です。例えばこんなものがあります。

  • メモリ
  • ファイル
  • ネットワーク
  • データベース

リソースは、使うときに確保し、使い終わったら開放する必要があります。そうしないと次の処理が使えません。

エラーの場合は処理中断するので、気を付けないとリソースを確保したまま開放されない状態になってしまいます。それが更に深刻なエラーを引き起こしかねません。

正常の場合と同様に、エラーの場合でも、リソースの開放は確実に行う必要があります。

大事なこと(3)エラー情報を記録する

エラーが発生したとき、その原因がすぐには分からないことがあります。

あとでエラー原因を分析するため、なにが起こったかを記録しておく必要があります

この記録のことを、ロギングといいます。たいていの場合、エラー情報をユーザーに見えないところにテキストファイルで残します。このファイルをログファイルといいます。

3つの考え方をプログラミングする

この3つの考え方、実際にどうプログラミングするのか、押さえておきましょう。

プログラミングでの、エラー処理の実装方法

エラー処理には、以下の2つが必要です。

  • エラーが発生したことを通知する
  • エラーに対して何か対処する

モジュール分割されたプログラムでは、この2つを、それぞれ別のモジュールで対処することが多いです。

実装方法は大きく分けて2通り。

プログラムにおけるエラー処理
  • 戻り値方式 戻り値リターンでエラーを伝え、リターンしたモジュールでエラー対処
  • 例外方式 例外スローでエラーを伝え、例外キャッチしたモジュールでエラー対処
  • 戻り値方式を取るのは、C、Go、Rustなど
  • 例外方式は多くの言語が採用しています。C++、Java、Python、Ruby、PHP、JavaScript、Kotlin、Swift…

プログラミング言語のエラー処理は、歴史的に、戻り値方式(C言語)から始まり例外方式(C++,Java…)が主流になり、また戻り値方式に戻る(Go,Rust)という歴史があります。ここでは、主流の例外方式で説明します。

Javaでの、例外のプログラム例です。

public class ExceptionHandlingExample {
    public static void main(String[] args) {
        try {
            // 例外が発生する可能性があるコードを記述する
            int result = divide(10, 0);
            System.out.println("結果: " + result);
        } catch (ArithmeticException e) {
            // 発生した例外がArithmeticExceptionの場合の処理
            System.out.println("0での割り算はできません。");
        }
    }

    public static int divide(int num1, int num2) {
        // 引数の検証
        if (num2 == 0) {
            throw new ArithmeticException("ゼロでの割り算はできません。");
        }

        // 割り算の実行
        return num1 / num2;
    }
}

エラー処理でなにをするのか、順番に見ていきましょう。

エラー処理実装(1)throwで、すみやかに中断する

エラーが発生したときは、throwで例外を発生させます。(例外を投げる、といった言い方もします)

throwが実行されると、次の処理には進まず、catchまで処理が飛びます。そのメソッド内にcatchがなければ、呼び出し元までさかのぼっていきます。

エラー処理実装(2)-1 finallyでリソース開放

多くのプログラミング言語では、リソース開放を確実に行うための手段を準備しています。

やりかたは大きく2種類。

ひとつめは、finally で確実にリソースを開放する方法です。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class FinallyExample {

    public static void main(String[] args) {
        FileInputStream file = null;
        try {
            file = new FileInputStream("example.txt");
            // ファイルを読み込む処理などを記述
        } catch (FileNotFoundException e) {
            System.out.println("ファイルが見つかりませんでした。");
        } finally {
            // ファイルのクローズ処理など、リソースの解放を行う
            if (file != null) {
                try {
                    file.close();
                } catch (IOException e) {
                    System.out.println("ファイルのクローズ中にエラーが発生しました。");
                }
            }
        }
    }
}

catchブロックは、throwが発生した場合のみ動きます。ここでエラー処理を行います。

finallyブロックは、throwが発生してもしなくても、必ず動きます

ここにリソース開放処理を入れることで、正常の場合でもエラーの場合でも、必ずリソースを開放できるようになります。

エラー処理実装(2)-2 try-with-resourceでリソース開放

もうひとつのリソース開放方法は、try-with-resourceと呼ばれる方法です。

tryブロックで、そのブロック中で確保するリソースを宣言すると、ブロック終了時に必ず開放します

tryブロックが終わるのは、ブロック内の最後の処理が終わった時、もしくはthrowでブロックを抜けるときです。

このどちらの場合であっても、tryブロックを抜けたタイミングで必ずリソースを開放します

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class TryWithResourcesExample {

    public static void main(String[] args) {
        try (FileInputStream file = new FileInputStream("example.txt")) {
            // ファイルを読み込む処理などを記述
        } catch (FileNotFoundException e) {
            System.out.println("ファイルが見つかりませんでした。");
        } catch (IOException e) {
            System.out.println("ファイルの読み込み中にエラーが発生しました。");
        }
    }
}

プログラミング言語ごとのリソース開放方法

リソース自動開放の仕掛けは、プログラミング言語によって、実装方法にかなりの違いがあります。

基本的なプログラミング構文は、どの言語もけっこう似てくるものですが、このリソース自動開放はほんとうにバリエーション豊か。

以下に一例をあげます。

リソース自動開放のバリエーション
  • C++ RAII
  • Java try-with-resource
  • C# using
  • Python with
  • Swift defer
  • Kotlin usr
  • Go defer
  • Rust Dropトレイト

エラー処理実装(3)ロギングライブラリによるエラー情報を記録

ログの出力は、一般的にはテキストファイルへのエラー情報の書き込みによって実現します。

ロギング処理(ログファイルへの出力)を、自前で実装することは避けましょう。

自前での実装は、以下のような問題があります。

  • エラー発生時の確実な書き込みができないときがある
  • 並列処理(マルチスレッドなど)のときに競合書き込みでクラッシュする恐れがある

必ずロギングライブラリを使いましょう。Javaであれば、log4j が有名です。

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class ExceptionHandlingSample {
    private static final Logger logger = LogManager.getLogger(ExceptionHandlingSample.class);

    public static void main(String[] args) {
        try {
            // 例外が発生する可能性がある処理
            divide(10, 0);
        } catch (ArithmeticException e) {
            // 例外が発生した場合の処理
            logger.error("Divide by zero error", e);
        }
    }

    public static int divide(int dividend, int divisor) {
        if (divisor == 0) {
            throw new ArithmeticException("Divide by zero");
        }
        return dividend / divisor;
    }
}

ロギング内容で、なによりまず大事なのは「エラーが発生した箇所を特定できること」、次に「エラー発生までの過程を追えること」です。

プログラミング言語ごとのスタックトレース取得方法

エラー発生時の過程を追うには、スタックトレースが便利です。スタックトレースで、エラー発生時に以下のことが分かります。

  • エラーが発生した場所(ファイル名、行番号、関数名やメソッド名など)
  • そのエラーを引き起こした関数やメソッドの呼び出し履歴
  • 各関数やメソッドの呼び出しに関連する情報(引数の値など)

多くのプログラミング言語で、エラー発生時にスタックトレースが自動的に収集されているので、それを利用できます。

スタックトレースのバリエーション
  • Java ExceptionクラスのprintStackTraceやgetStackTrace
  • C# ExceptionクラスのStackTraceプロパティ
  • Python tracebackモジュール
  • JavaScript Errorオブジェクトのstack
  • Swift Thread.callStackSymbols
  • Kotlin ExceptionクラスのprintStackTraceやgetStackTrace
  • Go runtime.stack、debug.PrintStack
  • Rust std::backtrace::Backtrace

意識的にログ出力処理を入れましょう。

まとめ

エラー処理は、どうしても面白味には欠けるので、軽視しがちなところがあります。

しかし、プログラムの健全性を保つために、なくてはならないものです。エラー処理の基本を押さえて、あなたのプログラムを頑健なものにしましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次