「障害時の復旧設計」を一言でいうと
障害時の復旧設計は、
「エラーが起きた“あと”に、アプリをどう立て直すかをあらかじめ決めておくこと」 です。
エラー処理というと、
つい「catch してログ出して終わり」にしがちですが、
本当に大事なのはその先です。
ユーザーの画面はどうなるのか。
中途半端な状態はどう片付けるのか。
もう一度やり直せるのか、それとも諦めるのか。
これを「その場のノリ」で決めるのではなく、
あらかじめ方針として持っておく——
それが「障害時の復旧設計」です。
まず「壊れ方」をイメージするところから始める
非同期処理は「中途半端な状態」が生まれやすい
非同期処理では、次のような「中途半端」がよく起きます。
ボタンを押して API を叩いたが、途中でエラーになった
一覧の一部だけ読み込めて、残りは失敗した
フォーム送信中にネットワークが切れた
このとき、アプリはこうなりがちです。
ローディング表示が消えない
ボタンが押せないまま
画面には古いデータが残っているが、ユーザーはそれを知らない
「成功したのか失敗したのか」が分からない
復旧設計とは、
「こういう中途半端な状態から、どう“まともな状態”に戻すか」 を決めることです。
「復旧できる障害」と「復旧できない障害」を分ける
全部を「自動で完全復旧」しようとすると、
設計が一気に難しくなります。
なので、まずはざっくりと、
ユーザーが再試行すれば復旧できるもの(ネットワーク一時障害など)
アプリ側の再試行で復旧できるもの(タイムアウト、リトライ可能な API など)
人間の対応や時間経過が必要なもの(サーバー障害、データ不整合など)
のように分けて考えます。
復旧設計は、
「全部を魔法のように直す」ことではなく、
「どこまで自動で頑張り、どこからはユーザーや運用に任せるか」を決めること でもあります。
一番基本の復旧設計:「安全な状態に戻す」
ローディングやボタン状態を必ず元に戻す
非同期処理でまずやるべき復旧は、
「UI の状態を元に戻す」ことです。
例えば、よくあるパターンがこれです。
async function onClickSave() {
disableSaveButton();
showLoading();
try {
await saveForm();
showSuccessMessage("保存しました");
} catch (err) {
console.error(err);
showErrorMessage("保存に失敗しました");
} finally {
hideLoading();
enableSaveButton();
}
}
JavaScriptここでのポイントは finally です。
try で成功しても、catch で失敗しても、
finally の中は必ず実行されます。
つまり、
ローディング表示を消す
ボタンを再び押せるようにする
という「復旧」が、
成功・失敗に関わらず必ず行われます。
ここが重要です。
障害時の復旧設計の最初の一歩は、
「どんなエラーが起きても、UI を“操作可能な状態”に戻す」ことです。
そのために finally を使うのは、非同期処理の鉄板パターンです。
中途半端なデータを捨てるか、残すかを決める
例えば、一覧画面で追加読み込みをしているときにエラーになったとします。
すでに表示されているデータは正しいが、
新しく読み込もうとした分だけ失敗した、という状態です。
このときの復旧方針は、ざっくり次の二択です。
既に表示されているデータはそのまま残し、「追加分だけ失敗しました」と伝える
一度全部クリアして、「再読み込みしてください」と促す
どちらが正解というより、
「この画面の性質的に、どちらがユーザーにとって安全か・分かりやすいか」で決めます。
例えば、
「銀行残高一覧」なら中途半端な表示は危険なので、
全部クリアして「再読み込みしてください」の方が安全かもしれません。
一方、
「ブログ記事一覧」なら、
既に見えている記事はそのまま残し、
「追加読み込みに失敗しました」とだけ出す方が親切かもしれません。
ここが重要です。
復旧設計は、「データをどう扱うか」の設計でもあります。
“壊れたかもしれないデータ” を残すのか捨てるのかを、画面ごとに意識して決めてください。
再試行(リトライ)を復旧の一部として設計する
自動リトライと手動リトライ
障害時の復旧でよく出てくるのが「再試行」です。
再試行には大きく二種類あります。
コード側が自動で再試行する(リトライ処理)
ユーザーに「もう一度試す」ボタンを出す(手動リトライ)
例えば、ネットワークが一瞬だけ不安定だった場合、
自動リトライはかなり有効です。
一方で、
サーバーが完全に落ちている場合に何度も自動リトライしても、
ユーザーを待たせるだけになります。
なので、
「どの種類のエラーに対して、何回まで自動リトライするか」
「それでもダメなら、ユーザーにどう伝えるか」
をセットで考えます。
シンプルな自動リトライの例
例えば、fetch を 3 回までリトライする関数を作ってみます。
async function fetchWithRetry(url, options = {}, maxRetry = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetry; attempt++) {
try {
const res = await fetch(url, options);
if (!res.ok) {
throw new Error("HTTP error " + res.status);
}
return res;
} catch (err) {
lastError = err;
console.warn(`[fetchWithRetry] 失敗 ${attempt} 回目`, err);
if (attempt < maxRetry) {
await new Promise((r) => setTimeout(r, 500));
}
}
}
throw lastError;
}
JavaScriptこの関数は、
最大 3 回まで試す
毎回失敗したら少し待ってから再試行
それでもダメなら最後のエラーを投げる
という復旧戦略を持っています。
呼び出し側は、普通の fetch と同じように使えます。
async function loadUsers() {
try {
const res = await fetchWithRetry("/api/users");
const data = await res.json();
renderUsers(data);
} catch (err) {
showErrorMessage("ユーザーの取得に失敗しました。時間をおいて再度お試しください。");
}
}
JavaScriptここが重要です。
リトライは「エラーをなかったことにする魔法」ではなく、
「一時的な障害からの復旧を少しだけ頑張る仕組み」です。
何回まで、どの間隔で、どの種類のエラーに対して行うかを、意識して決めてください。
ユーザーに「やり直しの手段」を渡す
自動リトライだけでなく、
ユーザーに「再読み込み」「もう一度送信」ボタンを出すのも立派な復旧設計です。
例えば、一覧画面でエラーになったときに、
画面上部にこういう UI を出します。
「データの取得に失敗しました。[再読み込み]」
コード的には、
エラー時に「再試行用の関数」をボタンに紐づけるだけです。
async function loadUsers() {
try {
setLoading(true);
const res = await fetch("/api/users");
if (!res.ok) throw new Error("HTTP error " + res.status);
const data = await res.json();
renderUsers(data);
hideErrorBanner();
} catch (err) {
console.error(err);
showErrorBanner("ユーザーの取得に失敗しました。", loadUsers);
} finally {
setLoading(false);
}
}
JavaScriptshowErrorBanner の中で、
「再試行」ボタンに loadUsers を紐づけておけば、
ユーザーは自分のタイミングで復旧を試せます。
ここが重要です。
復旧設計は、「コードが勝手に頑張る」だけでなく、
「ユーザーにやり直しの選択肢を渡す」ことも含みます。
そのための UI をどこに、どう出すかも設計の一部です。
状態の巻き戻しと「一貫性」の確保
部分的に成功したとき、どうするか
例えば、次のような処理を考えます。
ユーザー情報を更新する API を呼ぶ
続けて、プロフィール画像をアップロードする API を呼ぶ
途中で 2 番目だけ失敗した場合、
状態が「半分だけ更新された」ように見えるかもしれません。
フロントエンド側でできる復旧は限られますが、
少なくとも次のような方針は決められます。
画面上の表示を「サーバーの最新状態」に合わせて再取得する
「一部の更新に失敗しました」とユーザーに伝える
再度まとめて更新を試すためのボタンを出す
例えば、こういうコードになります。
async function updateProfile(form) {
setLoading(true);
try {
await callApi("/api/profile", {
method: "PUT",
body: JSON.stringify(form.basic),
});
await callApi("/api/profile/avatar", {
method: "POST",
body: form.avatarFile,
});
showSuccessMessage("プロフィールを更新しました");
} catch (err) {
console.error(err);
showErrorMessage("プロフィールの更新に失敗しました。一部が反映されていない可能性があります。");
await reloadProfileFromServer();
} finally {
setLoading(false);
}
}
JavaScriptここでは、
失敗したら「一部が反映されていない可能性」を正直に伝える
サーバーから最新のプロフィールを再取得して、画面表示をサーバーに合わせる
という復旧戦略を取っています。
ここが重要です。
障害時の復旧設計では、「画面の状態」と「サーバーの状態」の一貫性をどう保つかが重要です。
完全なトランザクションはフロントだけでは難しいですが、
少なくとも「画面をサーバーに合わせ直す」という復旧は意識しておくと良いです。
「諦める」ことも復旧設計の一部
復旧できないときの「落としどころ」を決める
どれだけ頑張っても、
復旧できない障害は必ずあります。
サーバーが完全に落ちている
重要なデータが壊れている
認証情報が不整合になっている
こういうときに大事なのは、
「無理に続けようとしない」 ことです。
例えば、
認証エラーが連続して起きる場合は、
「一度ログアウトしてログインし直してもらう」
というのが現実的な復旧策かもしれません。
async function loadSecureData() {
try {
const data = await callApi("/api/secure-data");
renderSecureData(data);
} catch (err) {
if (err instanceof AuthError) {
showErrorMessage("ログイン情報が無効になりました。再度ログインしてください。");
redirectToLogin();
return;
}
showErrorMessage("データの取得に失敗しました。時間をおいて再度お試しください。");
}
}
JavaScriptここでは、
「AuthError の場合は“復旧”ではなく“再ログインに誘導する”」
という方針を取っています。
これはある意味で「諦め」ですが、
「中途半端に壊れた状態で使い続けさせない」という意味で、
とても重要な復旧設計です。
エラーページへの退避も一つの復旧
どうにもならない致命的なエラーが起きたときは、
「エラーページに飛ばす」というのも立派な復旧です。
SPA であれば、
グローバルなエラーハンドラで「予期しないエラー」を捕まえ、
専用のエラーページコンポーネントに遷移させることもできます。
ユーザーから見れば、
「よく分からない壊れた画面」に取り残されるより、
「エラーが起きました。トップに戻る」
といった明確な状態に移った方が、ずっとマシです。
ここが重要です。
復旧設計は、「全部を元通りにする」ことだけではありません。
「これ以上壊れた状態で使わせないよう、安全な場所に退避させる」ことも含まれます。
初心者として「障害時の復旧設計」で本当に押さえてほしいこと
非同期処理では、
エラーが起きた瞬間だけでなく、「その後の状態」がとても重要になる。
まずは、
どんなエラーが起きても「UI を操作可能な状態に戻す」ことを徹底する。
ローディングを消し、ボタンを戻し、中途半端な状態を放置しない。
次に、
「再試行で復旧できるもの」と「できないもの」を分けて考える。
自動リトライを入れるか、ユーザーに再試行ボタンを渡すかを決める。
画面のデータとサーバーのデータがズレたときは、
「サーバーから再取得して画面を合わせ直す」という復旧を用意しておく。
どうにもならない場合は、
無理に続けず、「再ログイン」「エラーページ」「トップに戻る」など、
安全な落としどころを用意する。
そして何より、
catch の中で「ログを出す」「メッセージを出す」だけで終わらせず、
“このあとアプリをどう立て直すか” を必ず一度考えること。
コードを書いていてエラー処理に差し掛かったら、
自分にこう問いかけてみてください。
「このエラーが起きた“あと”、ユーザーの画面はどうなっていてほしい?」
その答えが、そのまま「復旧設計」になります。
そのイメージをコードに落としていく感覚を、少しずつ育てていきましょう。

