Newer
Older
hello-programmer-world / src / pages / tips / fix-error.mdx
@h.sakamoto h.sakamoto 21 hours ago 8 KB commit
---
layout: "@/layouts/MarkdownLayout.astro"
---

import Details from "@/components/Details.astro";
import Dialog from "@/components/Dialog.astro";

export const title = "エラーと向き合う";

# {title}

開発をしていると、必ずどこかしらでエラーに遭遇します。  
本番で発生するエラーは恐ろしいものではありますが、開発中に発生するエラーはむしろ頼もしい味方です。

ここではそのエラーに関する基本的な知識と、その扱い方について解説します。

## TOC

## よく使われる用語

### Exception / Error

プログラムの実行中にエラーが発生したときに、ただちに処理が中断されるわけではありません。  
それがどんなエラーなのか、どこで発生したのかなどの情報をまとめて、「例外(Exception)」や「エラー(Error)」として情報を開発者に伝えます。

何故かよく知られているのはJavaの`NullPointerException`で、いわゆる「ヌルポ」ですね。

これは、`null`に対して何らかの操作をしようとしたときに発生する例外なのですが、このように例外自体に名前がついていることが多いです。  
逆に言えばこの名前を見れば、どんなエラーなのかがわかるようになっています。

<Details summary="Javaの例">

```java file=/sample/tips/Main.java
public/sample/tips/Main.java
```

<br />

```bash title="コンソールの出力例" "Exception" "NullPointerException"
$ javac Main.java
$ java Main
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
        at Main.main(Main.java:4)
```

</Details>

<br />

<Details summary="JavaScriptの例">

```js file=public/sample/tips/sample.js
public/sample/tips/sample.js
```

<br />

```bash title="コンソールの出力例" "Error" "TypeError"
$ node sample.js
/path/to/project/public/tips/sample.js:2
console.log(str.length);
                ^

TypeError: Cannot read properties of null (reading 'length')
    at Object.<anonymous> (/path/to/project/public/tips/sample.js:2:17)
    at Module._compile (node:internal/modules/cjs/loader:1761:14)
    at Object..js (node:internal/modules/cjs/loader:1893:10)
    at Module.load (node:internal/modules/cjs/loader:1481:32)
    at Module._load (node:internal/modules/cjs/loader:1300:12)
    at TracingChannel.traceSync (node:diagnostics_channel:328:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:245:24)
    at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5)
    at node:internal/main/run_main_module:33:47
```

</Details>

<br />

どちらも文字列のような`str`に対してその長さを取得しようとして、その`str`の中身が`null`であるためにエラーが発生しています。  
エラーメッセージをきちんと読んでみると、どちらもそのことが書かれているのがわかりますね。

このような直接的な原因は、`Exception`や`Error`のキーワードに続くメッセージに書かれていることが多いです。  
なにかエラーが発生したときは、まずこのキーワードを探してみましょう。

### Stack Trace

<Details summary="phpの例">

```php file=public/sample/tips/sample.php
public/sample/tips/sample.php
```

  <br />

```bash {3-5}
$ php sample.php
PHP Fatal error:  Uncaught TypeError: Unsupported operand types: int + array in /path/to/project/public/sample/tips/sample.php:5
Stack trace:
#0 /path/to/project/public/sample/tips/sample.php(8): add()
#1 {main}
  thrown in /path/to/project/public/sample/tips/sample.php on line 5
```

</Details>

## エラーの原因を特定する

以下の例を見てみましょう。  
これは購入した商品の合計金額と、その商品の数から平均金額を計算して表示するPHPのプログラムです。

実際はログなどの出力や、ユーザーからの報告によってエラーを知ることになります。  
それを想定して、先にコンソールに出力されるエラーメッセージを見てみましょう。

```bash title="コンソールの出力例" wrap
PHP Fatal error:  Uncaught DivisionByZeroError: Division by zero in /path/to/project/public/sample/tips/fix-error.php:48
Stack trace:
#0 /path/to/project/public/sample/tips/fix-error.php(61): summary()
#1 /path/to/project/public/sample/tips/fix-error.php(66): main()
#2 {main}
  thrown in /path/to/project/public/sample/tips/fix-error.php on line 48
```

<br />

以下がそのソースコードです。  
解説の前に、実際になにが問題なのかを見つけてみましょう。

<br />

```php file=public/sample/tips/fix-error.php
public/sample/tips/fix-error.php
```

<br />

では解説します。

まずはどんなエラーが発生しているのかを確認します。

```bash wrap "DivisionByZeroError"
PHP Fatal error:  Uncaught DivisionByZeroError: Division by zero in /path/to/project/public/sample/tips/fix-error.php:48
Stack trace:
#0 /path/to/project/public/sample/tips/fix-error.php(61): summary()
#1 /path/to/project/public/sample/tips/fix-error.php(66): main()
#2 {main}
  thrown in /path/to/project/public/sample/tips/fix-error.php on line 48
```

`DivisionByZeroError`というエラーが発生していることがわかります。  
これは割り算をする際に、割る数が`0`である場合に発生するエラーです。

このエラー名から、具体的にどんな問題が起きているのかを推測できました。

<br />

次に、そのエラーがどこで発生しているのかを確認します。  
エラーから、以下の2箇所がその発生箇所を示しています。

```bash wrap "in /path/to/project/public/sample/tips/fix-error.php:48" /thrown in.*$/
PHP Fatal error:  Uncaught DivisionByZeroError: Division by zero in /path/to/project/public/sample/tips/fix-error.php:48
Stack trace:
#0 /path/to/project/public/sample/tips/fix-error.php(61): summary()
#1 /path/to/project/public/sample/tips/fix-error.php(66): main()
#2 {main}
  thrown in /path/to/project/public/sample/tips/fix-error.php on line 48
```

どちらも`fix-error.php`の48行目で発生していることを示していますね。  
その箇所には`$average = $total / count($bouthtItems);`というコードがあります。

```php file=public/sample/tips/fix-error.php {48} collapse={1-34, 55-66}
public/sample/tips/fix-error.php
```

PHPにおける`count`関数は、配列の要素数を返します。  
つまりこの引数になっている`$boughtItems`の要素数が`0`だったことが原因だと推測できます。

<br />

ここからは、なぜ`$boughtItems`の要素数が`0`になったのかの経緯を調べていきます。

まずこの変数がどこで設定されているのかを確認します。  
すると、関数の引数として外部から渡されていることがわかります。

```php file=public/sample/tips/fix-error.php collapse={1-34, 55-66} "$boughtItems"
public/sample/tips/fix-error.php
```

<br />

次に問題になるのは、この関数の引数に、空の配列を渡したのは誰か、ということです。  
ここで手がかりになるのが、スタックトレースです。

もう1度スタックトレースを見てみましょう。

```bash {2-5}
PHP Fatal error:  Uncaught DivisionByZeroError: Division by zero in /path/to/project/public/sample/tips/fix-error.php:48
Stack trace:
#0 /path/to/project/public/sample/tips/fix-error.php(61): summary()
#1 /path/to/project/public/sample/tips/fix-error.php(66): main()
#2 {main}
  thrown in /path/to/project/public/sample/tips/fix-error.php on line 48
```

これを見ると、summary関数が61行目で呼び出されていることがわかります。  
さらにその61行目はmain関数の処理のなかで呼び出されていて、このmain関数は66行目で呼び出されていることがわかります。

この情報をもとに、61行目付近のコードを見てみましょう。

```php file=public/sample/tips/fix-error.php {61} collapse={2-55} "$boughtItems"
public/sample/tips/fix-error.php
```

変数`$boughtItems`が渡されていて、この配列が空だったことが原因でエラーが発生したことがわかります。  
さらにはこの変数は`getBoughtItemsByUserId`関数の戻り値として設定されているところまでわかります。

<br />

ここまでの情報で、ここエラーはどういう状況で発生したのかがわかりました。  
これをまとめると、以下のようになります。

購入した商品が1つもない状態で平均金額を計算しようとしたために、`0`で割り算をしようとしてエラーが発生した。

これがこのエラーの原因です。  
これを踏まえて、今後の対策を考えていくことになるのです。