JavaScriptのエラーついて知っておくべきこと

# 最初に

この記事では、JavaScriptのエラーに関する知識やベストプラクティスについて記述しています。

# 文字列を直接throwしない
`throw`ステートメントを使用してエラーを発生させる際、次のように文字列を直接`throw`するべきではありません。

```javascript
throw 'やばいエラー💩';
```

これは、上記のように文字列を`throw`した場合、スタックトレースが取得できないためです。これでは、問題の発生箇所を特定することができません。

エラーを`throw`する場合は、以下のように`Error`オブジェクトを使用するのが正解です。

```javascript
throw new Error('やばいエラー💩');
```

なお、文字列の`throw`は[ESLintのルール(no-throw-literal)](https://eslint.org/docs/rules/no-throw-literal)で禁止させることができるので、ESLintを使用することでこのようなミスを防ぐことが可能です。

# 非同期のErrorはcatchできない
`setTimeout`のように非同期で発生した`Error`は、次のコードで示すように`try-catch`ステートメントでcatchすることができません。

```javascript
function throwError() {
  throw new Error('やばいエラー💩');
}

function run() {
  try {
    // 通常通りthrowErrorを実行
    throwError();

  } catch (error) {
    // throwErrorのErrorをcatchできる
    console.error(error);
  }
}

function runAsync() {
  try {
    // setTimeoutで1秒後にthrowErrorを実行
    setTimeout(throwError(), 1000);

  } catch (error) {
    // 非同期に実行すると、throwErrorのErrorをcatchできない
    console.error(error);
  }
}
```

ちなみに、`setTimeout()`を使用する場合、`Promise`を使用したラッパーを用意することで、`Error`をキャッチすることができます。

```javascript
// setTimeoutのラッパー
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function runAsync() {
  delay(1000)
    .then(throwError)
    .catch(error => {
      // Errorをcatchできる
      console.error(error);
    });
}
```

# thenの外側でErrorオブジェクトを生成する
まずは、以下のコードを見てみましょう。

```javascript
function fetchUser() {
  fetch('https://something.com/user')
    .then(response => {
      if (!response.ok) {
        throw new Error('エラーだよ');
      }
    })
}
```

このコードでは、`fetchUser()`の中で`fetch()`を使用してユーザデータを取得しています。また、`fetch().then()`の中では、レスポンスがOKではなかった場合に`Error`をthrowしています。この処理自体は決して間違いではありませんが、問題はここで作られた`Error`オブジェクトに`fetchUser()`を呼び出すまでのスタックが生成されないという点にあります。つまり、どのような順序を辿って最終的に関数が呼び出されたのか、判断が難しくなってしまうのです。`fetchUser()`を呼び出す次の処理を見て見ましょう。

```javascript
function funcA() {
  fetchUser();
}

function funcB() {
  funcA();
}

funcB();
```

上記のコードでは、`funcB()` -> `funcA()` -> `fetchUser()`という順序で最終的に`fetch()`を実行しますが、以下の図で示すエラーのように、それらの関数呼び出しがスタックに現れません。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12152/de31b2d1-6763-ba05-92d6-cd350569f386.png)
*※ thenの中でErrorを生成した場合のスタック*

そこで、コードを以下のようにします。

```javascript
function fetchUser() {
  // fetch()をコールする前にErrorオブジェクトを用意する
  // これによって、fetchUser()を呼び出すまでのstackが生成される
  const err = new Error();

  fetch('https://something.com/user')
    .then(response => {
      if (!response.ok) {
        err.message = 'エラーだよ';
        throw err;
      }
    })
}

function funcA() {
  fetchUser();
}

function funcB() {
  funcA();
}

funcB();
```

上記のように`fetch()`を呼び出す前に`Error`オブジェクトを生成しておくことで、`fetchUser()`を呼び出すまでのスタックが`Error`に生成されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12152/706f4424-3de5-5d46-89e3-5d56a4eaf7c3.png)
*※ thenの外側でErrorを生成した場合のスタック*

この`fetch`のように、非同期処理を伴う場合は`Error`オブジェクトを事前に生成しておくことで、エラー発生までの経緯を明確にすることができます。

# awaitにはtry-catchを使用する
`await`を使用して非同期処理を実行した場合、呼び出した処理がrejectされるとそれ以降の処理は実行されません。

```javascript
function throwErrorFromPromise() {
  return new Promise(resolve => {
    throw new Error('やばいエラー💩');
  });
}

async function run() {
  await throwErrorFromPromise();

  // 次のコードは実行されない
  console.log('😟');
}

run();
```

このようなことが起こる理由は、`await`によって呼び出された処理がrejectされた場合、[rejectされた値を`throw`して](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await)それ以降の処理を実行しないためです。そのため、`await`で呼び出した処理がrejectされても処理を継続させる場合は、`try-catch`でエラーハンドリング処理を記述しておく必要があります。

```javascript
async function run() {
  try {
    await throwErrorFromPromise();
  } catch(error) {
    // エラーハンドリング処理
  }

  // 次のコードは実行される
  console.log('😀');
}
```

# その他
以下の記事では、捕捉されなかったJavaScriptエラーのデータ収集方法について述べています。こちらも一読することをお勧めします。
[ユーザのブラウザで起きた JavaScript のエラーを収集する](https://qiita.com/grapswiz/items/71f14e73820e352c1fa3)


# 参考
- [JavaScript: Dirty Parts of the Language (aka Maneuvering JavaScript Errors)](https://www.youtube.com/watch?v=IcYRsNB3zD8)
- [Handle error from setTimeout - Stack Overflow](https://stackoverflow.com/questions/41431605/handle-error-from-settimeout)
- [Capture and report JavaScript errors with window.onerror](https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror.html)