フロントエンドにおけるエラーハンドリング
例えばアプリを操作していて、
・どこをクリックしても反応しない
・ローディングが回ったまま
などが起きるとユーザーが戸惑ってしまい、UX的に良くない。
何かしらの処理が失敗した際に、ユーザー(UI側)にきちんと伝えることがフロントエンドにおけるエラーハンドリング
UI側にエラーを表示するメリット
・処理に失敗したことを伝えることで、再試行すればいいという事がわかる
・エラー発生時にエラーコードなどを出しておけば、問い合わせ時にどこで起きたエラーなのかを判別しやすく解決しやすくなる
エラーハンドリングの実装が必要なケース:
・APIとの通信処理(データが取れなかったり、リクエストが失敗した時)
・ライブラリの初期化
など
エラーハンドリングの実例:
-
APIがレスポンスオブジェクトに失敗か、成功かを返してくれる
isOK, isSuccessとかのフラグを返してくれる設計であればそれを使う一番シンプルな単一のエンドポイントを呼ぶとき
const result = await fetchData()
If (!result.isSuccess) {
// エラー表示処理を呼ぶ
}
- データ量が多くて、分割でリクエスト
Promise.allで複数のAPIを並列で呼ぶ場合
const results = Promise.all([
fetchData1,
fetchData2,
fetchData3,
])
const flatResults = results.flat()
const errorResponse = flatResults.find((res) => !res.isSuccess)
If (errorResponse) {
// エラー表示処理を呼ぶ
}
results
にはレスポンスが配列で入るので、全て失敗か?一部失敗か?どちらでエラー判定するかは処理の性質による。
try catch文
try catch文は、予想外のエラーを意図的に回避するための処理です。
つまり、エラーが発生するかもしれない箇所をtryブロックに記述し、プログラムが異常終了しないような回避策をcatchブロックの中に記述します。
例外がある場合:try
→ catch
→ finally
の順
例外がない場合:try
→ finally
の順
現在の現場では、エラーオブジェクトを返してくれないAPIとのやり取りでエラーハンドリングするためにtry catch文を使ってます。
try-catch-finallyの順をくずしてはダメで、必ずこの順番で書きます。
try {
// 例外エラーが発生するかもしれない処理
} catch(e) {
// 例外エラーが起きた時に実行する処理
} finally {
// 例外発生の有無に関係なく最後に実行される
}
また、tryブロックの中で、try catchを書いて入れ子にする事も可能です。
if文などで分岐する事が多い
try {
if (条件1) {
try {
// 例外エラーが発生するかもしれない処理 1
} catch(e) {
// 例外エラーが起きた時に実行する処理
}
}
if (条件2) {
try {
// 例外エラーが発生するかもしれない処理 2
} catch(e) {
// 例外エラーが起きた時に実行する処理
}
}
} catch(e) {
// 例外エラーが起きた時に実行する処理
}
throw
throw
文は、ユーザー定義の例外を発生させます。 現在の関数の実行を停止し(throw の後の文は実行されません)、コールスタック内の最初の catch ブロックに制御を移します。呼び出し元の関数に catch ブロックが存在しない場合は、プログラムが終了します。
要は throw
すると意図的に例外を発生させて処理をそこで終了させる事ができます。
以下サンプルの様に、tryブロックでthrow
すると、catchブロックに処理が進み、throwしたエラーを受け取れます。
try {
// 4回繰り返し、jに加算する
let j = 0;
for (let i = 0; i < 5; i++) {
j += i
}
// jが5より大きい場合、例外を発生させる
if( j > 5 ) {
throw new Error('jが5より大きいです。')
}
// throwしたので、ここまで処理は到達しない
alert(j)
} catch (e) {
//例外エラーがおきたら、コンソールにログを出力する
console.error("エラー:", e);
}
*後続処理に行かないので、後続処理へ進めたいときはthrowしないで、エラーログを出力だけする等がいい。上記のサンプルの場合、if( j > 5 )
の中身をエラーログ出力に変えれば、後続処理の alert(j)
も動きます。
if( j > 5 ) {
console.error('jが5より大きいです。')
}
catchブロックでキャッチした例外を判定して、スローを分岐する実装も一般的です。
try {
// 例外エラーが発生するかもしれない処理
} catch (e) {
if (e.code === 403) {
// 403エラーの場合のスロー
} else {
// 403以外の場合のスロー
}
}
また、throw
はtry catchの中で多く使われていますが、それ以外の場所でも使えます。
throwしたらプログラムが終了し、後続処理に進みません。
以下のケースだと、2番目のコンソールは出力されません。
console.error("1番目")
throw new Error('例外をスロー')
console.error("2番目")
- 例外をthrowして、処理を止めるか?
- 後続も進めるか?
はその処理の性質によると思います。
リトライ処理
これはフロントエンドではなく、API開発の時の事例で、DBへの保存など、ミスしてほしくない場合、エラーが発生しても、決まった回数分ループして繰り返すリトライ処理がよく使われるようです。
以下の例は最大d3回繰り返し、全て失敗した場合はエラーを出して終了
const MAX_RETRIES = 3
let result = undefined
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
result = await updateHoge(hogeData)
return result
} catch (e) {
if (attempt >= MAX_RETRIES - 1) {
// エラーログを出して終了
}
}
}
まとめ
-
エラー処理は意識して作らないといけない
-
エラー処理は書かなくても成功すれば動いてしまうので、サボってしまいがちだが、ユーザー体験を下げてしまうので、手間がかかってもやるべき
-
正常処理を書き、例外処理を考える