AIに「APIからデータ取ってきて」と頼むと、こういうコードが返ってくる
Claude Code や ChatGPT に「fetch でAPIからデータ取ってきて。エラー処理もつけて」とお願いすると、だいたいこんなコードが返ってきます。
async function getUser(id: string) {
try {
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
return user;
} catch (error) {
console.error("取得に失敗しました", error);
throw error;
}
}
try-catch で囲まれていて、一見ちゃんとエラー処理されているように見えます。レビューでも「まあ大丈夫そう」と通してしまいがちです。
でも、このコードには落とし穴があります。サーバーが 404 や 500 を返したとき、この catch には入りません。 エラー扱いにならず、成功したかのように後続処理へ進んでしまいます。
なぜそうなるのか、Before → After のコード対比で見ていきます。
Before(悪い例): なぜ 404/500 を見逃すのか
もう一度、さっきのコードです。
async function getUser(id: string) {
try {
const res = await fetch(`/api/users/${id}`);
// ❌ res が成功レスポンスか失敗レスポンスかを一切見ていない
const user = await res.json();
return user;
} catch (error) {
console.error("取得に失敗しました", error);
throw error;
}
}
問題の根っこは、fetch() の仕様にあります。
fetch()は、HTTPステータスが 404 や 500 でも Promise を reject しない(=例外を投げない)。
fetch()が reject するのは、ネットワークエラー(オフライン・DNS解決失敗・CORS違反など)や、リクエストが中断されたときだけ。
これは MDN にも明記されている挙動です。サーバーにリクエストが「届いて」レスポンスが「返ってきた」時点で、ステータスが何であれ fetch() の Promise は resolve(成功) します。404 だろうと 500 だろうと、「通信としては成功した」という扱いなのです。
つまり Before のコードでは、こういうことが起きます。
- サーバーが
404 Not Foundを返す - でも
fetch()は reject しないのでcatchに入らない - そのまま
res.json()に進む - 404 のレスポンスボディ(HTMLのエラーページや
{ "error": "not found" }など)を無理やり JSON パースしようとする - 運が良ければ
res.json()がパースエラーで落ちる/運が悪いと{ error: "not found" }を「ユーザーデータ」として返してしまう
後者が一番厄介です。エラーがエラーとして扱われず、変なデータが下流に流れていく。 バグの発生箇所と実際に問題が顕在化する箇所がズレるので、原因究明にすごく時間がかかります。
catch があるから安心、ではないんですね。catch が拾ってくれるのは「ネットワークが死んでるとき」だけで、「サーバーがエラーを返したとき」は素通りします。
After(良い例): res.ok を明示的にチェックする
正しくは、res.ok でHTTPレベルの成否を自分でチェックし、失敗なら自分で throw する ことです。
async function getUser(id: string) {
try {
const res = await fetch(`/api/users/${id}`);
// ✅ HTTPレベルの失敗(4xx/5xx)を明示的にチェックする
// res.ok は status が 200〜299 のとき true
if (!res.ok) {
// status を含めて投げると、後で原因が追いやすい
throw new Error(`APIエラー: ${res.status} ${res.statusText}`);
}
const user = await res.json();
return user;
} catch (error) {
// ここにはネットワークエラー(fetch自体のreject)と
// 上で自分が throw したHTTPエラーの両方が入る
console.error("取得に失敗しました", error);
throw error;
}
}
ポイントは2つです。
-
res.ok: ステータスが 200〜299 のときtrue、それ以外(404, 500 など)はfalse。これで「HTTPレベルで成功したか」を判定できる。 -
!res.okのときに自分でthrow: fetch は投げてくれないので、自分で投げてはじめてcatchに入る。エラーメッセージにres.statusを含めると、404 なのか 500 なのかがログで一目で分かる。
これで、try-catch が ネットワークエラー(fetch の reject) と HTTPエラー(自分で throw した 4xx/5xx) の両方をカバーできるようになりました。
ステータス別に分岐したいなら
「404 は空扱いにしたいけど 500 は例外として投げたい」みたいに、ステータスごとに振る舞いを変えたいこともあります。その場合は res.status を見て分岐します。
async function getUser(id: string) {
try {
const res = await fetch(`/api/users/${id}`);
if (res.status === 404) {
// 「見つからない」は正常系として扱う設計もあり
return null;
}
if (!res.ok) {
// 404以外の失敗(500 など)は例外にする
throw new Error(`APIエラー: ${res.status} ${res.statusText}`);
}
return await res.json();
} catch (error) {
console.error("取得に失敗しました", error);
throw error;
}
}
res.ok(成功か否かのざっくり判定)と res.status(具体的な番号)を使い分けるのがコツです。
実務でのひとこと
いくつか補足しておきます。
-
async/awaitでも.then()でも挙動は同じ。 どちらで書いても、fetch が 4xx/5xx で reject しないのは変わりません。.then()派でもres.okチェックは必要です。 -
axiosは挙動が違う。 axios はデフォルトで 4xx/5xx を受け取ると Promise を reject します(catchに入る)。なので「axios の感覚で fetch を書くと res.ok チェックを忘れる」という取りこぼしが起きやすい。ライブラリを乗り換えたときは特に注意です。 - HTTPステータスの範囲:
1xx情報 /2xx成功 /3xxリダイレクト /4xxクライアント側エラー /5xxサーバー側エラー。res.okがtrueになるのは2xxだけです。
AIにfetchを書かせるときのレビュー観点
AIコード生成と付き合ううえで、fetch は「一見正しく見えて実は抜けがある」典型例です。生成されたコードをそのまま信じず、res.ok のチェックが入っているかを目視する のをレビュー項目に入れておくと事故が減ります。
抜けていたら、こう追加でお願いすると堅くなります。
このfetchのコードに、res.ok のチェックとHTTPエラー(404/500)の分岐を追加してください。エラー時は res.status をメッセージに含めて throw してください。ネットワークエラーの try-catch はそのまま残してください。
AI は「動くコード」は速く出しますが、「HTTPの失敗をどう扱うか」みたいな仕様の判断は、指示しないと最小限で済ませがちです。ここを人間側のレビュー観点として持っておくと、プログラミングそのものと、AIにどう指示すれば堅いコードが返ってくるかという協働スキルが同時に身につきます。
fetch の1行から、AIとの付き合い方まで地続きだったりします。
中上級者の方には当たり前すぎる内容なので、初心者の知り合いへの紹介や、社内研修・後輩育成の資料の参考にでもどうぞ。
未経験から Next.js + Supabase + Claude Code で Web アプリを公開するまで を、手を動かしながら学べる導線を用意しています。
- 無料で試せる体験版(GitHub): https://github.com/ayies128/next-ai-camp-trial
- 教材+メンタリング(MENTA): https://menta.work/plan/20251?ref=qiita
- YouTube『AIエンジニア情報局』(※MENTAとは別運営の発信チャンネルです)
このあたりの「AIが書いたコードのどこを疑うか」は、教材の中でも実際のプロンプトと一緒に扱っています。