1つの catch にまとめると、会員情報保存の失敗が全て「会員登録の失敗」に化ける
- 59 分前
- 読了時間: 3分
会員登録のライトボックスから2つのことを続けて行うよう実装していた。アカウントを作ることと、所属先をプロフィールへ保存することである。
素直に書けば、全体を1つの try で囲んで、失敗したら「登録に失敗しました」と出す形になる。この形には、実害のある副作用がある。
2段目で落ちたとき、1段目はもう終わっている
アカウントの作成に失敗した場合、会員は作られていない。もう一度やり直すのが正しい。
会員の所属先の保存に失敗した場合、会員はすでに存在する。ログインもできている。やり直すべきなのは保存だけで、登録ではない。
この2つを同じ文言で表示すると、利用者は登録からやり直す。今度はアカウントの作成が重複で失敗する。画面には、また「登録に失敗しました」と出る。抜け出す道が無い。真因は最初から2段目にあるのに、表示がそれを隠している。
段階名を持って進む
そこで、進んでいる段階を変数に持ち、失敗の詳細に必ず段階名を添えるようにした。
let phase = 'ログイン状態の確認';
try {
let member = await getLoggedInMember();
if (!member) {
phase = '会員登録(register)';
try {
result = await authentication.register(email, password, { … });
} catch (err) {
// ここに来るのは「会員が作られなかった」場合だけ
console.error('register に失敗:', err);
setMessage(toRegisterErrorMessage(err), `register: ${errorDetail(err)}`);
return;
}
member = await getLoggedInMember();
}
phase = '所属先の保存(saveProfile)';
…
} catch (err) {
setMessage('登録処理でエラーが発生しました。', `${phase}: ${errorDetail(err)}`);
}内側の catch は、その段階でしか起きない失敗だけを受け持つ。外側は、想定していない失敗を段階名つきで拾う受け皿になる。画面の詳細欄に register: と出るか saveProfile: と出るかで、次の一手が変わる。
途中まで進んだ状態から再開できるようにする
段階を分けると、中断した状態をどう扱うかという問題が見えるようになる。前回の登録が保存の前に落ちて、ログインだけ残っている利用者がいる。
この人がもう一度登録ボタンを押したとき、アカウントの作成から始めてはいけない。すでに会員だからである。だから最初にログイン状態を確かめて、会員なら作成を飛ばして保存だけやり直す。
1つの try で囲んでいたときは、この分岐を書く動機すら生まれなかった。どこまで進んだかを持っていないのだから、途中から再開するという発想が出てこない。
戻り値ではなく、状態で判定する
もう1つ、この処理で戻り値を信用していない箇所がある。会員登録の API が、値を返さずに解決することがあるからである。エディタのプレビューでは会員 API が完全には動かないと公式にも書かれていて、実際に戻り値が undefined になる。
そこで、成否は戻り値ではなく、ログインできたかどうかで判定した。返ってきた状態は、案内の出し分けにだけ使う。承認待ちなのか、そもそも結果が確認できなかったのかで、利用者に伝えるべきことが違うからである。
返らなかったことにも意味を持たせる
段階を返す設計には、もう1つ効果がある。返ってこなかったこと自体が情報になる。
別のバックエンド関数で、段階ごとの結果を配列に積んで返すようにしていたとき、その配列が1件も返らないことがあった。0件ということは、1段目に入る前に落ちている。つまり原因は処理の中ではなく、本体を囲む関数の外か、モジュールの読み込みそのものにある。実際、原因はモジュールの評価時に外部へ触っていたことだった。
全体を1つの try で囲んで「失敗しました」とだけ返す設計では、この推論はできない。失敗の形が1種類しかないからである。
コメント