こんにちは、皆さん素敵なエラーハンドリングされていますか?
今担当しているプロジェクトのエラーハンドリングを改善すべく、今日も戦いましたので戦記を残しておきます。
今日のテーマは、
redux-sagaでfetchエラー(ネットワークエラー)になった時、どうやってエラーハンドリングする?
です。
前提
各バージョン
- react ^16.13.1
- redux-saga ^1.1.3
- react-redux ^7.2.1
説明簡略化の為・プロジェクトの歴史的経緯の為に下記について割愛している部分もありますのでご了承ください。
・actionCreaterを別ファイルに定義しなさい!
・constractorを別ファイルに定義しなさい!
・400ステータス等のエラーハンドリングも実装しなさい!
・flux-standard-actionに則りなさい!
などなど..
問題: 下記のコードを実行するとどうなる?
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;
});
};
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
ページへ遷移する、という流れになっています。
早速ややこしいと嫌われそうなので図にしてみました。
うまくerrorに誘導できそうですね。
早速ありもしないURLのfetchを実行してみると...
const result = yield call(fetchData,'http://tekitotekito')
...
/errorページに無事遷移できます!!
めでたし。めでたし。
...であればよかったのですが...
その例外処理まずいんじゃない?
これだと、コンソールに何もエラーが表示されず、一体何が原因で起きたエラーなのかさっぱりわからないですよね。
デバックしづらく後々地獄をみることになります。改善します。
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
そしてリロードすると...
真っ白ですね。
sagaの仕組み
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
で囲ってしまえばよい訳ですね。
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でエラーを出しておきます。
ページ内に表示したりエラーによって挙動を変えたり、ここは様々な対応ができます。
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です。
function* safe(effect) {
try {
return { result: yield effect }
} catch (err) {
return { err }
}
}
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内でのエラーハンドリングなので、詳しい説明は割愛します。