React / Redux Sagaで完璧なネットワークエラーハンドリングを実装したい!!

こんにちは、皆さん素敵なエラーハンドリングされていますか？
今担当しているプロジェクトのエラーハンドリングを改善すべく、今日も戦いましたので戦記を残しておきます。

今日のテーマは、
**redux-sagaでfetchエラー（ネットワークエラー）になった時、どうやってエラーハンドリングする？**
です。

## 前提

各バージョン
- react ^16.13.1
- redux-saga ^1.1.3
- react-redux ^7.2.1

説明簡略化の為・プロジェクトの歴史的経緯の為に下記について割愛している部分もありますのでご了承ください。
・actionCreaterを別ファイルに定義しなさい!
・constractorを別ファイルに定義しなさい!
・400ステータス等のエラーハンドリングも実装しなさい!
・[flux-standard-action](https://github.com/redux-utilities/flux-standard-action)に則りなさい!
などなど..

## 問題: 下記のコードを実行するとどうなる？

```react
function* fetchLogin() {
  while(true){
    const result = yield call(fetchData,'http://~~~')
    if (result) {
      yield put({
        type: 'IS_LOGIN_STATE', list: result
      });
    } else {
      // エラー時 下記putとリダイレクトの処理
      yield put({
        type: 'RESPONSE_ERROR', flg: true,
      });
    }
  }
}

const fetchData = (url) => {
  return fetch(url)
    .then((res) => {
      return res.json()
    }).catch((error) => {
      return false;
    });
};
```

```react:IS_RESPONSE_ERRORを受け取るsaga
function* resError() {
  while (true) {
    const result = yield take('RESPONSE_ERROR')
    yield put({ type: 'IS_RESPONSE_ERROR', flg: true })
    yield put(push('/error'))
  }
}
```

軽く流れを説明すると、`http://~~~`をfetchしてそれがネットワークエラー
（URLが間違っている/サーバーが落ちている等でレスポンスすらない状態）であれば、`RESPONSE_ERROR`がdispatchされ、
それを受けて`IS_RESPONSE_ERROR`をdispatchしつつ`/error`ページへ遷移する、という流れになっています。
早速ややこしいと嫌われそうなので図にしてみました。

![Untitled Diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441432/ed0fa331-3496-fb6b-e179-f988638dc89f.png)

うまくerrorに誘導できそうですね。
早速ありもしないURLのfetchを実行してみると...

```react
const result = yield call(fetchData,'http://tekitotekito')
...
```

**/errorページに無事遷移できます!!**
めでたし。めでたし。
...であればよかったのですが...

## その例外処理まずいんじゃない?
これだと、コンソールに何もエラーが表示されず、一体何が原因で起きたエラーなのかさっぱりわからないですよね。
デバックしづらく後々地獄をみることになります。改善します。

```diff
export const fetchData = (url) => {
  return fetch(url, {
    mode: 'cors',
    credentials: 'include',
  })
    .then((res) => {
      return res.json()
    }).catch((error) => {
-      return false
+      throw new Error(error);
    });
};
```

先ほどのfetchのcatch部分を例外をスローするよう変更します。
[throw - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/throw)
そしてリロードすると...
![スクリーンショット 2020-10-01 11.28.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441432/3dcb663b-184a-1244-f903-5ef83fba41dd.png)


真っ白ですね。

## sagaの仕組み
https://redux-saga.js.org/docs/basics/ErrorHandling.html
 
>Errors in forked tasks bubble up to their parents until it is caught or reaches the root saga. If an error propagates to the root saga the whole saga tree is already terminated.

フォークされたタスクがエラーになると、どこかでキャッチされない限り親のsagaに到達するまでバブリングし、親にまで到達した場合saga全体が終了してしまうからです。
saga全体がエラーで終了してしまい、Reactに影響を及ぼし真っ白になってしまった、というあたりでしょうか。

https://redux-saga.js.org/docs/basics/ErrorHandling.html
公式に丸っと求めていた例が乗っていました。
バブリングしないように、
`try ~ catch`で囲ってしまえばよい訳ですね。

```diff
function* fetchLogin() {
- if (result) {
+  try {
    const result = yield call(fetchData, `${USER}aaa`)
    yield put({
      type: 'IS_LOGIN_STATE_NULL',
      isLoggedIn: false,
      list: [],
      isFetching: true,
    });
+  } catch (e) {
+    yield put({
+      type: 'RESPONSE_ERROR',
+      error: e,
+    })
+  }
- }
}
```

ひとまずconsoleでエラーを出しておきます。
ページ内に表示したりエラーによって挙動を変えたり、ここは様々な対応ができます。

```diff
function* resError() {
  while (true) {
    const result = yield take('RESPONSE_ERROR')
    yield put({ type: 'IS_RESPONSE_ERROR', flg: true })
+    console.error(result.error)
    yield put(push('/error'))
  }
}

```

これで無事に、他のsagaを殺さずエラーの報告を出しつつ
ページ遷移を行うことができました。

## (ついで)sagaでバブリングして全体停止するのを常にやめたい
https://github.com/redux-saga/redux-saga/issues/1250
答えはここに全て書いてあります。
各sagaのタスクを`try ~ catch`するラッパー関数で包んであげればOKです。

```react:ラッパー
function* safe(effect) {
	try {
		return { result: yield effect }
	} catch (err) {
		return { err }
	}
}
```

```react
function* someSaga() {
  // resultはタイプエラー or 結果が返ってくる
  const result = yield safe(call(someAPI, arg1))
　// タイプエラーだったら親を殺さずに停止する
  yield safe(fork(otherSaga, arg2)) 
}
```

## (ついで) React v16からエラー処理が変更になった
https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html
v16以前はどこかでJS内のどこかでエラーが起きてもそのまま描写していた（＝不具合のある画面を見せていた）のですが、v16ではエラーが起きた場合は全ての描写がアンマウントされるようになりました。

React曰く、不具合のある画面を見せ続けるより全てアンマウントしてしまった方が安全であり、不具合が起こったタイミングを特定できやすいということです。

しかしこれではユーザーは困惑してしまいますね。
そこで`Error Boundary`というものが用意されています。
https://ja.reactjs.org/docs/error-boundaries.html

今回はSaga内でのエラーハンドリングなので、詳しい説明は割愛します。