JavaScript中級 #6 fetch API とエラー処理
#5 オプショナルチェーンと nullish 合体に続いて、今回はJavaScriptで外部の世界と通信するツール。ブラウザと Node の両方で動作する標準 fetch API。
fetch 基礎 #
const response = await fetch('https://api.example.com/users');
const users = await response.json();
console.log(users);三行で終わります。fetch は Promise を返すので — await で解きます(#2 非同期入門で見たパターン)。
fetch の結果は Response オブジェクトです。レスポンスボディをどう解釈するかによってメソッドが異なります。
await response.json(); // JSON パース
await response.text(); // テキストそのまま
await response.blob(); // バイナリ (画像/ファイルなど)
await response.arrayBuffer(); // ArrayBuffer (低レベル)
await response.formData(); // FormData
落とし穴 1 — 4xx/5xx は catch では捕まらない #
JavaScript の fetch は HTTP ステータスコードが 4xx/5xx でも reject しません。ネットワーク自体が失敗したときだけ reject します。
try {
const res = await fetch('/api/not-found');
// ステータスが 404 でもここまで来る
console.log(res.status); // 404
} catch (err) {
// ネットワーク失敗でなければ入ってこない
}この動作はほかの言語/ライブラリ(axios など)と異なり、混乱をよく引き起こします。ステータスチェックを自分でしなければなりません。
const res = await fetch('/api/users');
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();res.ok はステータスが 2xx の範囲なら true。このチェックを抜かすと 4xx レスポンスをそのまま JSON パースしようとして別のエラーにつながります。
メソッドとボディ — POST/PUT/DELETE #
基本は GET。ほかのメソッドを使うにはオプションオブジェクトを二番目の引数として渡します。
const res = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'カーティス', age: 30 }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const created = await res.json();三つの核心:
method— 既定 ‘GET’、POST/PUT/PATCH/DELETE などheaders—Content-Typeでボディ形式を明示body— 送る本文。オブジェクトは直接送れずJSON.stringifyで文字列化
FormData を送る #
ファイルアップロードのように multipart ボディが必要なら:
const formData = new FormData();
formData.append('name', 'カーティス');
formData.append('avatar', fileInput.files[0]);
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
// Content-Type はあえて書かない — ブラウザが自動設定
});FormData を送るときは Content-Type を直接書きません。ブラウザが boundary を含んだ正確なヘッダーを自動で作ってくれます。
ヘッダーと認証 #
const token = localStorage.getItem('token');
const res = await fetch('/api/me', {
headers: {
'Authorization': `Bearer ${token}`,
},
});クッキーを一緒に送るには credentials オプション:
const res = await fetch('/api/me', {
credentials: 'include', // 別の origin にもクッキーを送る
});既定値は 'same-origin' (同じドメインのときだけクッキー)。外部 API がクッキー認証を使うなら 'include' が必要です。
クエリパラメータ — URLSearchParams
#
URL にクエリ文字列を付けるときに直接文字列を作るのは危険です。URLSearchParams がエンコーディングまで処理してくれます。
const params = new URLSearchParams({
q: '検索語 with spaces',
page: '1',
sort: 'date',
});
const res = await fetch(`/api/posts?${params}`);
// /api/posts?q=%E6%A4%9C%E7%B4%A2%E8%AA%9E+with+spaces&page=1&sort=date
+、&、日本語、空白すべて自動的に URL エンコードされます。
AbortController — リクエスト取り消し #
長くかかるリクエストを途中で取り消したいとき。
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // 5秒後にキャンセル
try {
const res = await fetch('/api/slow', {
signal: controller.signal,
});
console.log(await res.json());
} catch (err) {
if (err.name === 'AbortError') {
console.log('キャンセル済み');
} else {
throw err;
}
}これが#2 非同期入門で少し言及した timeout のモダンな答えです。Promise.race + タイマーよりすっきりしています。
AbortSignal.timeout() — ES2022
#
よく使うパターンなので短い形も追加されました。
const res = await fetch('/api/slow', {
signal: AbortSignal.timeout(5000),
});5秒超過時に自動で abort。AbortController を直接作る必要がありません。
同時リクエスト取り消し — コンポーネントアンマウントパターン #
React コンポーネントがアンマウントされたのに、その間進行中だった fetch の結果が遅れて到着すると、なくなったコンポーネントに setState を呼んで警告が出ます。AbortController が標準的な答えです。
function UserPage({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${id}`, { signal: controller.signal })
.then((r) => r.json())
.then(setUser)
.catch((err) => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort(); // cleanup
}, [id]);
// ...
}return () => controller.abort() が核心。アンマウントや依存変更時に進行中だった fetch が取り消されます。
ストリーミングレスポンス — for await
#
大きなレスポンスを chunk 単位で受け取りたいとき。
const res = await fetch('/api/large');
for await (const chunk of res.body) {
console.log(chunk); // Uint8Array の断片
}res.body は ReadableStream。for await (#3 イテレータとジェネレータ)で chunk を順番に受け取ります。AI streaming レスポンス、大きなファイル、Server-Sent Events のような場面で出会います。
Node での fetch #
昔の Node では fetch がなくて node-fetch パッケージをインストールする必要がありました。Node 18 から fetch がビルトインです。追加インストールなしですぐ使えます。
const res = await fetch('https://api.example.com/users');
const users = await res.json();スクリプト、CLI ツール、サーバーサイドすべて同じ API で通信できます。
ロギングとリトライ — 小さな wrapper #
複数の箇所で同じパターン(ステータスチェック、エラーロギング)が繰り返されるなら wrapper 関数を作るのがすっきりします。
async function api(url, options = {}) {
const res = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${url}`);
}
return res.json();
}
// 使用例
const users = await api('/api/users');
const created = await api('/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'カーティス' }),
});これをさらに発展させると — リトライ、タイムアウト、認証トークン自動追加などを一箇所で管理できます。大きなアプリならば結局似た wrapper が自分なりの形で作られます。
ライブラリを使うか、fetch だけで行くか #
axios、ky、ofetch のような fetch の上に載せるライブラリも多いです。それぞれのトレードオフ:
| 選択肢 | 利点 | 欠点 |
|---|---|---|
ビルトイン fetch | 依存関係0、標準 | 4xx チェック直接、ボイラープレート |
| ky | 小さくモダン、fetch ベース | 依存関係追加 |
| axios | 豊富な機能、昔から標準 | 重く、カスタム動作が多い |
| TanStack Query | キャッシング/同期まで | 学習コスト |
小さなプロジェクト、ライブラリ、学習コードは fetch だけで十分です。大きなアプリなら通常 wrapper またはデータフェッチライブラリ(TanStack Query)と組み合わさる流れです。
まとめ #
この記事で整理した内容:
fetchは Promise を返し、レスポンスはResponseオブジェクト.json()、.text()、.blob()でボディを解釈- 4xx/5xx は自動で throw されない —
res.okを自分でチェック - POST は method/headers/body オプション +
JSON.stringify - FormData は Content-Type 自動
URLSearchParamsで安全なクエリエンコーディングAbortController/AbortSignal.timeout()で取り消しfor await (chunk of res.body)でストリーミング- Node 18+ もビルトイン fetch
- 小さな wrapper / TanStack Query のようなライブラリは大きなアプリで価値あり
次の記事(#7 JSON の扱いとシリアライズ)では、JavaScript データを JSON でやりとりするときによく出会う場面 — parse/stringify のオプション、Date/特殊値処理、深いコピー トリックの落とし穴まで整理します。