なぜ「例外仕様の文書化」がそんなに大事なのか
非同期処理は「うまくいくとき」だけ見ていると、わりと簡単そうに見えます。await fetch(...) して、res.json() して、画面に出す——これで終わり。
でも、実際の現場では「うまくいかないとき」が必ずあります。
ネットワークエラー、タイムアウト、サーバーエラー、パース失敗、権限エラー…。
ここで重要になるのが
「この非同期関数は、失敗したときにどう振る舞うのか?」を、コードと文章で“仕様として”はっきりさせること
です。
それが「例外仕様の文書化」です。
動けばいい、ではなく
「失敗の仕方まで含めて設計し、それを他人が分かる形で残す」
ここから一気に“プロの設計”に近づきます。
まず「この関数はどう失敗しうるか」を言語化する
成功パスだけでなく「失敗パス」を列挙する
例として、ユーザー情報を取得する非同期関数を考えます。
async function fetchCurrentUser() {
const res = await fetch("/api/user");
if (!res.ok) {
throw new Error("ユーザー取得に失敗しました");
}
return res.json();
}
JavaScriptコードだけ見ると「失敗したら Error を投げる」ことは分かりますが、
どんな状況で、どんな種類のエラーが飛んでくるのかは分かりません。
ここで一度、頭の中でこう整理します。
ネットワーク自体が失敗したらどうなるか
HTTP ステータスが 401, 403, 500 のときはどう扱うか
レスポンス JSON が壊れていたらどうなるか
これを「なんとなく」ではなく、
「この関数の仕様」として書き出すのが、例外仕様の文書化です。
文章にするときのイメージ
例えば、こんな感じの説明になります。
fetchCurrentUser()は、以下のいずれかの結果になる:成功時:ユーザーオブジェクトを解決する Promise を返す
失敗時:
・ネットワークエラーの場合、TypeError(fetchが投げるもの)をそのまま伝播する
・HTTP ステータスが 2xx 以外の場合、Errorを投げる(メッセージにステータスコードを含める)
・レスポンス JSON のパースに失敗した場合、SyntaxErrorをそのまま伝播する
ここまで書いてあると、呼び出し側は
「何を try/catch すべきか」「どんなメッセージをユーザーに見せるか」
を設計しやすくなります。
「投げる」のか「返す」のかを仕様として決める
例外で表現するスタイル
非同期関数の失敗を表現する方法は、大きく二つあります。
一つ目は「例外を投げる」スタイルです。
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error(`ユーザー取得に失敗しました: ${res.status}`);
}
return res.json();
}
JavaScriptこの場合の仕様は、例えばこう書けます。
fetchUser(id)は以下のときに例外を投げる:・ネットワークエラーが発生した場合:
TypeError
・HTTP ステータスが 2xx 以外の場合:Error(メッセージにステータスコードを含む)
・レスポンス JSON のパースに失敗した場合:SyntaxError
呼び出し側は、こういうコードになります。
try {
const user = await fetchUser(1);
renderUser(user);
} catch (err) {
showErrorMessage(err.message);
}
JavaScript結果オブジェクトで表現するスタイル
もう一つは、「成功・失敗をオブジェクトで返す」スタイルです。
async function fetchUserSafe(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
return {
ok: false,
error: new Error(`ユーザー取得に失敗しました: ${res.status}`),
};
}
const user = await res.json();
return { ok: true, user };
} catch (err) {
return { ok: false, error: err };
}
}
JavaScript仕様としては、こう書けます。
fetchUserSafe(id)は常に成功として解決される Promise を返す。
返り値は{ ok: true, user }または{ ok: false, error }のどちらか。
例外は外側には投げない(すべてerrorプロパティに包む)。
呼び出し側は、こうなります。
const result = await fetchUserSafe(1);
if (!result.ok) {
showErrorMessage(result.error.message);
} else {
renderUser(result.user);
}
JavaScriptここで一番大事なのは
「この関数は“投げるのか”“返すのか”を、仕様としてはっきり決めて、一貫させること」
です。
どちらも混在させると、呼び出し側が地獄を見ます。
例外仕様を「コードの近く」に書く
JSDoc 風に書いてみる
ドキュメントは README に書いてもいいですが、
一番効くのは「関数のすぐ上」に書くことです。
/**
* 現在ログイン中のユーザーを取得する。
*
* 成功時:
* - ユーザーオブジェクトを解決する Promise を返す。
*
* 失敗時:
* - ネットワークエラー: fetch が投げる TypeError をそのまま伝播。
* - HTTP ステータスが 2xx 以外: Error を投げる(メッセージにステータスコードを含む)。
* - JSON パースエラー: SyntaxError をそのまま伝播。
*/
async function fetchCurrentUser() {
const res = await fetch("/api/user");
if (!res.ok) {
throw new Error(`ユーザー取得に失敗しました: ${res.status}`);
}
return res.json();
}
JavaScriptこうしておくと、
エディタ上で関数にカーソルを合わせたときに、
「どう失敗しうるか」が一瞬で分かります。
ここが重要です。
「例外仕様は“頭の中の暗黙ルール”ではなく、“コメントとしてコードにくっついているルール”にする。」
「呼び出し側の視点」で書く
文書化するときは、
実装者の視点ではなく「呼び出す人の視点」で書くのがコツです。
悪い書き方の例は、こうです。
fetch が失敗したらそのまま throw します。
res.ok が false のときは Error を throw します。
これだと、「で、呼び出し側は何を想定すればいいの?」が分かりません。
良い書き方は、こうです。
この関数を呼び出す側は、以下のエラーを catch しうる:
・ネットワークエラーの場合: TypeError
・HTTP エラーの場合: Error(メッセージにステータスコードを含む)
・レスポンス JSON が壊れている場合: SyntaxError
つまり、
「この関数を使う人が、try/catch の中で何を想定すべきか」を書いてあげる
イメージです。
非同期チェーン全体で「どこまで伝播させるか」を決める
どこで「握りつぶすか」を仕様として決める
非同期処理は、関数が何段階も重なります。
async function fetchCurrentUser() { ... }
async function loadUserPageData() {
const user = await fetchCurrentUser();
const posts = await fetchUserPosts(user.id);
return { user, posts };
}
async function initUserPage() {
const { user, posts } = await loadUserPageData();
renderUser(user);
renderPosts(posts);
}
JavaScriptここで大事なのは、
「どのレイヤーまで例外をそのまま伝播させて、どのレイヤーで UI 向けのエラーに変換するか」
を決めることです。
例えば、こう決めることができます。
fetchCurrentUser / fetchUserPosts
→ 技術的なエラー(ネットワーク・HTTP・JSON)をそのまま投げる
loadUserPageData
→ それらをまとめて「ユーザーページの読み込みに失敗しました」という Error に包み直す
initUserPage
→ その Error を catch して、ユーザーに見せるメッセージに変換する
仕様としては、こう書けます。
fetchCurrentUser/fetchUserPosts
- 技術的なエラーをそのまま投げる。UI 向けのメッセージは持たない。
loadUserPageData
- 下位のエラーを受け取り、
Error("ユーザーページの読み込みに失敗しました")に包み直して投げる。
initUserPage
loadUserPageDataのエラーを catch し、ユーザー向けのエラーバナーを表示する。
ここが重要です。
「どこまでが“技術的な例外”、どこからが“ユーザー向けのエラー表現”かを、レイヤーごとに決めて文書化する。」
例外仕様とテストをセットで考える
「この仕様なら、こういうテストを書くよね」を意識する
例外仕様をちゃんと書くと、
自然と「こういうテストが必要だな」が見えてきます。
例えば、fetchUserSafe の仕様がこうだとします。
fetchUserSafe(id)は常に{ ok: boolean, user?, error? }を返す。
例外は外に投げない。
ネットワークエラー・HTTP エラー・JSON エラーはすべてok: falseとして返す。
この仕様に対して、テストはこうなります。
成功時:ok === true で user が入っている
ネットワークエラー時:ok === false で error が入っている
HTTP エラー時:同上
JSON エラー時:同上
つまり、
「例外仕様は、そのままテストケースのリストになる」
ということです。
仕様を書いていないと、
テストも「なんとなく書いた」ものになりがちです。
初心者として「例外仕様の文書化」で本当に意識してほしいこと
最後に、あなたの頭の中に置いておいてほしい問いをまとめます。
この非同期関数は、成功したとき“だけ”でなく、失敗したときにどう振る舞うかを説明できるか。
この関数は「例外を投げる」のか、「結果オブジェクトで返す」のか、一貫しているか。
呼び出し側は、どんなエラーを catch しうるのか、文章で書けるか。
どのレイヤーで技術的なエラーを UI 向けのエラーに変換するか、決めてあるか。
その仕様を、コメントやドキュメントとして「コードの近く」に残しているか。
おすすめの練習は、
自分の非同期関数を一つ選んで、関数の上にこう書いてみることです。
「この関数は、どんなときに、どんなエラー(または結果)を返すのか」
それを日本語で書き切ってから、
「じゃあ実装はこの仕様にちゃんと合っているか?」
と見直してみてください。
そのサイクルを回し始めたとき、
あなたはもう「とりあえず try/catch を書く人」ではなく、
「失敗の仕方まで設計し、言葉にできるエンジニア」 に近づいています。
