プログラミングで発生する「エラー」に立ち向かう秘伝書

## はじめに
この記事はプログラミングとは切っても切れない「エラー」解決のための思考法とデバッグ手法の記事となっております。
基本的には開発環境や言語に依存しない汎用的な範囲での表現であることをご承知おきください。

## 要点のまとめ
エラーに立ち向かう最短のルートは<strong>「一発でエラーの発生個所と原因を特定し、適切な対応を行う事」</strong>です。
エラーの解決に必要な情報がそろっており、対策もわかっていれば後はそれを実行するのみです。

一方で
* エラーの発生箇所が分からない
* エラーの原因が分からない
* エラーに対する対応方法が分からない

場合はまずは<strong>「仮説を立てて検証する」</strong>ことにより発生個所の特定及びエラーの原因を探っていきます。
ここでのポイントは
* 正常に動作するためには<strong>「ここは○○であるはず」(前提条件)</strong>を確認する
* 「○○であるべき」を逆転させて<strong>「○○かもしれない」(仮説)</strong>を洗い出す
* 「○○かもしれない」を否定するための<strong>「○○を調べたから違う」(検証方法)</strong>を考える

です。

例
```mermaid
graph TD;
    A[テキストファイルの読み込みに失敗した]-->B1[ファイルのパス指定は正しいはず];
    B1-->C11[パス指定が違うかもしれない];
    B1-->C12[ファイルが存在しないかもしれない];
    C11-->D111[読込先に指定しているパスにファイルがあれば違う]
    C12-->D111
    A-->B2[ファイルの読み込み処理は行われているはず];
    B2-->C21[そもそも処理が行われていないかもしれない];
    C21-->D211[ブレークポイント等により処理を通っていることが確認できれば違う];
```

そして、なかなか解決できない場合は
* 公式が提供してるドキュメントやFAQなどを見る
* 同じ現象に陥った人の解決方法を探す
* 人に相談する
* 今まで立てた仮説と検証結果の中でおかしな点が無いか見直す(認識の違いで誤った判断をしている可能性もある)
* スタート地点に戻ってみる

など周りの力も借りつつ取組みましょう。

## 詳細
ここからはより具体的にエラーの調査と対応方法について説明していきます。

### その１　エラーを大まかに分類する
#### エラーの大分類
プログラミングにおいて発生する「エラー」には主に３種類あります。
<dl>
    <dt>構文エラー(文法エラー)</dt>
    <dd>
        <ul>
            <li>括弧やクオーテーションが閉じられていない</li>
            <li>文末にセミコロンがついていない</li>
        </ul>
        などの<strong>プログラミング言語の仕様に沿った書き方から外れた部分がある場合のエラー</strong><br/>
        <strong>シンタックスエラー(Syntax Error)</strong>とも呼びます。<br/>
        エラーメッセージにこのワードが出てきたら<br/>
        まずは間違っている構文が無いかを確認すべきです。<br/><br/>
    </dd>
    <dt>コンパイルエラー</dt>
    <dd>
        <ul>
            <li>DLLの読み込みに失敗した</li>
            <li>存在しないプロパティやメソッドを使用しようとした</li>
        </ul>
        などの<strong>コンパイル時に発生するエラー</strong><br/>
        ※コンパイル：プログラミング言語のソースコードを実行ファイルに変換する作業<br/>
        javascript,Python等のインタプリタ型言語の場合はソースコードを1行ずつ読込み実行するため<br/>
        コンパイルエラーは存在しません。<br/><br/>
    </dd>
    <dt>実行時エラー</dt>
    <dd>
        <ul>
            <li>メモリ不足</li>
            <li>数値を"0"で割り算しようとした</li>
        </ul>
        などの<strong>プログラム実行中に発生するエラー</strong><br/>
        <strong>ランタイムエラー(Runtime Error)</strong>とも呼ばれます。<br/>
        プログラミング言語によっては<strong>例外(Exception)</strong>というものに変換されて<br/>
        開発者が発生したエラーに対し何かしらの対応を行えるような仕組みを用意しています。<br/>
        例外が発生した際にどのような対応を取るか実装したものを<strong>例外処理</strong>と呼びます。<br/>
        (エラーログの出力、警告メッセージを出すなど）<br/>
    </dd>
</dl>

:::note warn
構文エラーもコンパイル時にエラーとなりますが、本記事ではコンパイルエラーとは別として扱います。
:::

これ以外に
* 合計金額の計算が間違っている
* 出力した帳票のレイアウトがずれている

など処理自体は正常に終了しているもののシステムが想定通りに動いていない状況を
この記事では<strong>不具合</strong>と呼びます。

#### エラーの確認方法について
構文エラーの多くはIDE(統合開発環境)やエディタがエラーを発見しアラートを出してくれます。
また、コンパイルが必要な言語の場合、構文エラーとコンパイルエラーはコンパイル時に発覚します。
IDEやターミナルなどにエラー内容が出てくるはずですのでこれがエラー解決の大きな手掛かりとなります。
例えば画像のようなエラーメッセージをエディタが出してくれた場合
「8行目の赤い波線の部分がダメ」「ダメな理由として"sumAAAAAAAAAAA"が認識できていないんだな」
と察しが付くはずです。
![キャプチャ1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/e8ea5805-3ef0-a362-67a1-73525b15dfb4.png)
一方、実行時エラー及び不具合は処理を実行しないと浮かび上がらないので動作確認(テスト)を行う事で見つけていきます。
ログファイルなどにエラーの内容を書き出すように処理を追加しておくことで、テスト後に確認することが可能となります。
また、IDEなどに備わっている<strong>デバッグ実行機能</strong>を用いれば発生したエラーの内容の確認が容易になります。

 
:::note warn
本記事ではテストについては省略します。
すなわち動作確認(テスト)を行った結果エラーが見つかった後の対応方法について触れるものとします。
:::

#### 参考図
```mermaid
graph TD;
    A([IDE,エディタ,コンパイラが構文チェック])--実装ミス-->構文エラー;
    A--問題なし かつ コンパイルしない言語の場合-->C([作成したプログラムの実行]);
    A--問題なし-->B([コンパイラが解析し実行ファイルに変換]);
    B--失敗-->コンパイルエラー;
    B--問題なし-->C;
    C-->実行時エラー;
    C-->不具合;
    C-->正常終了;
```

#### この章のまとめ
> * 一言にエラーといっても大まかな分類がある
> * 構文エラーやコンパイルエラーはプログラム実行前に発覚することが多い(言語の仕様による)
> * 実行時エラーや不具合はテストを重ねながら探す必要がある

### その２　エラーに関する情報を整理する
#### 「どこで」「何が」起きたのか
エラーについてまず抑えておきたい情報は
* どこでエラーが発生しているか
* 何というエラーが発生しているのか

の2つになります。
「事件が発生しました」という報告だけでは警察は動けません。
「渋谷のハチ公前でひったくり事件が発生しました」のように事件の現場と内容は把握しておく必要があります。

エラーメッセージに乗っている情報の中で場所の特定の参考になるのは

* <strong>エラーが発生しているファイル名と行数</strong>
* <strong>エラーが発生している関数（メソッド）の名前</strong>

です。エラーの内容については

* <strong>エラーメッセージそのもの</strong>
* <strong>(実行時エラーの場合)例外の種類(○○Exception)</strong>
* <strong>エラーの対象となっているもの(オブジェクト名、クラス名など)

を見つけておけば今何が起きているのかを把握しやすいと思います。

:::note warn
エラーの内容をエラーメッセージから把握するのはもちろんのこと、それ以外にも
プログラムを実行した環境やデータの状態、自分がどのような行動をシステムに対して行ったのかなど
後々エラーの調査や対応に必要そうな情報も併せてまとめておくと尚良いです。
:::

#### この章のまとめ
> * 「どこで」「何が」「どのようなエラーを起こした」を整理する
> * エラーメッセージを読むことが解決への第一歩である
> * エラーメッセージが英語でも目を背けないで、翻訳したり調べる癖をつける

### その３　エラーの原因になっている箇所を検討する
#### エラーの原因の可能性は多岐にわたる
エラーが発生した箇所に実装上の問題があり、そこを修正することでエラーが解消されたのであれば問題ありません。
しかし、「エラーが発生している箇所」＝「エラーの原因になっている箇所」かと言われると一概にそうではありません。
エラーとなった処理より前の段階の処理に問題があったのが原因という可能性も十分にあります。
例えば、2つの文字列をそれぞれ整数値に変換して足した値を求める以下のような処理があったとします。

```mermaid
graph TD;
    A["①変数Aにファイル(aaa.txt)から取得した値を格納"]-->B["②変数Bにファイル(bbb.txt)から取得した値を格納"]-->C["③変数AとBをそれぞれ整数値に変換して足した値を変数Cに格納"];
```

この処理を実行すると3つ目の処理「変数AとBをそれぞれ整数値に変換して足した値を変数Cに格納」でエラーが発生しました。
内容は「数値に変換することが出来ません」と出ています。
いったい何が原因なのでしょうか？

可能性をリストアップすると
・<strong>整数値に変換するための関数の使い方を間違えており、引数に不適切な値を渡していた</strong>
・<strong>「変数Aと変数Bを整数値に変換する」はずが、「変数Aと変数Cを整数値に変換する」としてしまっていた</strong>
など③の処理に問題がある可能性もありますが

・<strong>aaa.txtを読み込んでいたつもりがccc.txtを読み込んでいた</strong>
・<strong>変数Aにaaa.txtの中身を格納するのではなく変数Bに中身を格納していた</strong>
といった前の処理に誤りがあるパターンや
・<strong>そもそもaaa.txt又はbbb.txtのファイルの中身が数字ではない</strong>
などプログラム内ではない場所に原因がある可能性もあります。

では、このたくさんある可能性の中から本当の理由を探すために何から始めればよいでしょうか？
キーワードは「仮説」と「切り分け」です。

#### 手順１ 処理の流れを整理する
まず、プログラムというものはいくつかの処理によって成り立っています。
そしてその処理は「前提」によって成り立っている部分が大きいです。
先ほどの処理を成功させるためには
* ①の処理で、<strong>ファイルaaa.txtの読み込みに成功するはず。</strong>テキストファイルには<strong>数字のみの値が1つ記述されているはずなので</strong>その値が文字列として<strong>変数Aの中に格納されているはずである。</strong>
* ②の処理は<strong>①の処理とほぼ同じ</strong>で、ファイル名はbbb.txt、変数Bに格納されているはずである。
* ①と②の処理を行った結果、<strong>変数Aと変数Bには文字列の数字が格納されているはず</strong>である。数値として足し算をするためにはまずこれらの文字列を数値の型に変換する必要がある。変換するためには標準機能で備わっているtoNumber関数(仮)を使用する。このtoNumber関数は引数が1つだけあり、文字列の数字を渡してあげると返り値として数値が返ってくる。なので<strong>その数値同士を足し算して、結果を変数Cに格納すれば求められた要件が達成できるはず</strong>だ

という設計になっています。
逆にいうとこの中の少なくともどれか1つにズレが生じた結果このロジックは破綻してエラーが発生したともいえます。

#### 手順２ エラーの原因の可能性(仮説)を列挙する
手順１で本来あるべき姿がまとまったので、これを基にエラーの原因の可能性を列挙します。

#### 手順３ 思いついた可能性の中で、確認が容易なものは先に確かめておく
例えばですが「変数Aに入れるはずが変数Bに入れていた」
などのレベルであればソースコードをさっと眺めるだけで気づくことが出来ます。
そういったものは先に調べておくと良いでしょう。
ソースコードを目で追いながらバグを探す方法を**机上デバッグ**と呼びます。

#### 手順４ 切り分けの方法をまとめる
プログラムの行数が多ければ多くなるほど、また可能性の範囲が広ければ広いほど愚直に確かめていると
時間がどんどん無くなってしまいます。
手順２で挙げたたくさんの可能性を効率よくつぶすためには切り分けを行ったうえで調査をする順番を決めることが重要です。

**分割統治法**という手法があります。これは大きな問題を小さな問題に分割することで原因の特定を目指すものです。
今回の場合大きな問題は<strong>「ファイルから読み込んだであろう数値の文字列を数値に変換することが出来ない」</strong>となります。
これを大局的に分割すると
<strong>「ファイルから数値を取得する際に問題がある」</strong>
または<strong>「文字列を数値に変換する処理そのものに問題がある」</strong>となります。
例えば処理①と②のファイルから文字列の数値を取得する処理がそれぞれ成功できている場合
変数Aにはaaa.txt内の数値、変数Bにはbbb.txtの数値が文字列として入っているはずです。
その確認が取れれば、「ファイルから数値を取得する」処理に問題が無いという判断を行うことが出来ます。
手順５の調査において、処理がどこまで進んだ時に、何を確認すれば上記のような問題の切り分けが可能かを検討します。

#### 手順５ 調査を開始する
調査を行うためにはデバッグログを埋め込むのが一般的です。
また、IDEなどに備わっている<strong>デバッグ機能</strong>を用いれば発生したエラーの内容の確認が容易になります。
デバッグ機能では
* ブレークポイント(指定した行で処理を一時的に停止できる)
* ステップ実行機能(1ステップごとに自分のタイミングで処理を進めることが出来る)
* 処理途中で変数に入っている値などの閲覧

などの機能があるため、調査を進めるうえで非常に助かります。


```mermaid
graph TD;
    S[エラーの発生]-->1;
    1([処理内容をまとめる])-->2([原因の可能性の列挙]);
    2-- ある程度洗い出せた-->3([すぐ確認できる内容の確認]);
    3-- 原因が見つかった-->G[原因の特定];
    3-- 原因が見つからなかった-->4([切り分けの方法検討]);
    4-->5([切り分け方法に沿って調査]);
    5-- 原因が見つかった-->G[原因の特定];
    5-- 原因が見つからなかった-->2;
    5-- 原因が見つからず今後苦戦しそう-->E2([他人の力を借りる]);
    2-- 苦戦している-->E[他人の力を借りる];
```

:::note warn
原因の究明を進めた結果、迷宮入りしてしまう事もあります。
いくら探しても原因が分からない場合は
・この問題は現在の環境で再現が可能なのか確認する。（調査している環境だと正常に動く可能性）
・一人で問題を抱えるのではなく、様々な人に情報を共有する
・今までに調査した内容を他の人に説明することで、「まだ試していなかった解決法」、「新たなアイディア」、「さらなる問題」が浮かぶことを期待する（ラバーダッグ・デバッグ）
・一度休憩し、頭の中をリセットする。
・原因を深堀りし過ぎてマニアックな部分にたどり着いた場合、初歩的な部分の確認漏れが無いか探す
などを行うとよいです。
:::

#### この章のまとめ
> * エラーの原因は多岐にわたるため、まずは考えられる可能性を仮説として列挙する
> * 机上デバッグでソースコードを見ればわかる原因を探す
> * 分割統治法の考えで問題を大局的に切り分けして調査し、範囲を絞ってから細かい部分の調査を行う

### その４　エラーに対する対応方法を検討する
エラーの原因が判明したら対応方法の検討に入ります。
詳細は省きますが、発生したエラーについて以下の観点で対応を検討するのが好ましいです。
#### 対応した際の影響範囲はどのくらいか
局所的であれば問題ないですが、複数の機能にまたがっていたりすると修正及び動作確認の工数も増えます。
納期や予算が決まっているようなシステム開発の場合は対応工数は重要な要素の一つなので影響範囲の調査は必要です。
（そもそも工数とか関係なく多機能での不具合を防ぐ意味でも重要です）

#### 想定内のエラーか、想定外のエラーか
想定内のエラーというのは通常の運用でも場合によっては起きる可能性のあるエラーの事を指します。
例えば10万件のデータを登録処理する中でどうしても入力不備などで1件ほどエラーになってしまう処理があったとします。
1件のために10万件すべての登録処理をなかったことにするのではなくこのエラーとなった1件のみを登録しないようにする等
エラーが起きることを想定さえできていれば様々な対応の検討を行う事が可能です。

想定外のエラーは通常の運用では発生する事が無いエラーのことで、想定できていないエラーに対し柔軟な措置を行う事は
困難であるため基本的には処理を中断してエラーとして扱うことが多いです。

#### どうにかできるのか、諦めた方がよさそうか
例えば外部提供のAPIやライブラリを使用しているときなど、
自分たちの力ではどうしようもできない様な部分でエラーが発生する事があります。
決められた仕様の中でエラー対策が出来るのであればよいですが、どうしてもエラーが発生してしまう場合は
そもそもの造りから見直しを掛けたり、プログラム自体はエラーとして運用で吸収するなどの
対応方法も候補にあがります。


**この章のまとめ**
> * エラーに対してシンプルな方法で対応するとどのくらいの影響範囲になるのかをまずは調査する
> * エラーは時に課題となり、プログラム修正以外の対応方法も候補にあがる
> * 想定外のエラーに対してもエラーハンドリングを行った方が良い


## まとめ
見たことがないエラーでも、原因は意外と単純であることもあるためまずは落ち着いて何が起きているのかを整理することが大切です。
後はここに記載したデバッグの手法を参考にしていただき、一つでも多くのエラーがなくなれば幸いです。

## おまけ１ 経験について
開発年数を重ねると無論多くの経験を積むことになります。
蓄積されたナレッジはプラスに働くこともありますがマイナスに働くこともあります。
「そういえば昔こんなことあったよな」という過去の記憶がトリガーとなり可能性の数が膨れ上がってしまう事です。
膨れ上がれば膨れ上がる分調査の優先度付けは大変になるので、昔の引き出しを探すのは調査をある程度終えてからの方がいいと個人的には思っています。
無論、経験を積んだ分仮説立てと優先度付けの精度も上がってデバッグの手際が良くなるはずですので経験＝悪というわけではないです。

## おまけ２ 他人から相談されたら
前述の通りエラーを解決するために他人の知見を得ることは大切です。
知見を得るというと相談してきた人に今までなかった新たな知識を授ける必要があるのかと思う方がいるかもしれませんが
相談された人の役割はそこではなく、第三者の視点で切り分けを行う事だと考えています。

まずは状況について細かく質問を行い、何を確認したのかを明確にしてあげます。
この明確にするフェーズの中で相談してきた人が自己解決するパターンも結構あります。
ある程度ヒアリングが終わったら、得た情報を元にまだ確認が出来ていない部分を検討し
相談してきた人へ次のアクションのアドバイスをしてあげるのが相談された側にとってベストの対応だと思っています。