fetch
ブラウザからの HTTP 通信のデファクト API。Promise ベースで、Node.js / Bun / Deno でも標準で使える。 XHR の後継。
基本
const res = await fetch("/api/users")
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const users = await res.json()
Response の主なメソッド
res.json()— JSONres.text()— テキストres.blob()— Blobres.arrayBuffer()— バイナリres.formData()— FormDatares.body— ReadableStream(ストリーミング)res.ok— status が 200..299 かres.status/res.statusTextres.headers— Headers オブジェクトres.url— 最終 URL(リダイレクト後)res.redirected— リダイレクトされたかres.type— basic / cors / opaque 等
ボディは1度しか読めない。複数回必要なら res.clone()。
POST / PUT
// JSON
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice", email: "a@b.c" }),
})
// FormData(ファイルアップロード等)
const fd = new FormData()
fd.append("file", file)
fd.append("title", "My Photo")
await fetch("/upload", { method: "POST", body: fd })
// Content-Type は自動で設定される(multipart/form-data + boundary)
// URLSearchParams(フォーム標準)
const usp = new URLSearchParams()
usp.append("q", "hello")
await fetch("/search", { method: "POST", body: usp })
クエリパラメータ
const url = new URL("/api/search", location.origin)
url.searchParams.set("q", "hello")
url.searchParams.set("page", "1")
const res = await fetch(url)
Headers
// オブジェクトリテラルで
fetch(url, { headers: { "X-Custom": "value", "Accept": "application/json" } })
// Headers クラス
const h = new Headers()
h.set("Authorization", `Bearer ${token}`)
h.append("X-Custom", "a")
h.append("X-Custom", "b") // 複数値
fetch(url, { headers: h })
// レスポンス側
res.headers.get("content-type")
res.headers.has("x-foo")
for (const [k, v] of res.headers) console.log(k, v)
credentials(Cookie の送受信)
credentials: "same-origin"(既定)— 同一オリジンのみ送るcredentials: "include"— 別オリジンにも送る(CORS 設定が必要)credentials: "omit"— 送らない
await fetch("https://api.example.com/me", {
credentials: "include", // クロスオリジンに Cookie を送る
})
cache モード
cache: "default"(既定)— ブラウザの通常キャッシュcache: "no-store"— キャッシュしない / 読まないcache: "no-cache"— 必ずサーバ確認(304 OK)cache: "reload"— 強制取得cache: "force-cache"— キャッシュを優先cache: "only-if-cached"— キャッシュのみ(オフライン用)
mode
"cors"(既定)— クロスオリジンを CORS で扱う"no-cors"— opaque レスポンス(中身読めない、画像/script など限定的)"same-origin"— 同一オリジンのみ"navigate"— ナビゲーション専用
redirect
"follow"(既定)— 自動追従"manual"— 追従しない(type: "opaqueredirect")"error"— リダイレクトでエラー
AbortController(タイムアウト・キャンセル)
const ac = new AbortController()
setTimeout(() => ac.abort(), 5000) // 5 秒タイムアウト
try {
const res = await fetch(url, { signal: ac.signal })
// ...
} catch (e) {
if (e.name === "AbortError") console.log("timeout")
else throw e
}
// 最近は AbortSignal.timeout が使える
const res = await fetch(url, { signal: AbortSignal.timeout(5000) })
複数の signal を組み合わせ
const signal = AbortSignal.any([userAbort.signal, AbortSignal.timeout(10000)])
fetch(url, { signal })
ストリーミング
res.body は ReadableStream。チャンクごとに処理:
const res = await fetch("/big.ndjson")
const reader = res.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
console.log(decoder.decode(value, { stream: true }))
}
SSE 風に行ごと処理
async function* lines(stream) {
const reader = stream.getReader()
const decoder = new TextDecoder()
let buffer = ""
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const parts = buffer.split("\n")
buffer = parts.pop() // 最後の不完全行
for (const p of parts) yield p
}
if (buffer) yield buffer
}
for await (const line of lines(res.body)) {
console.log(line)
}
進捗(プログレス)
fetch 自体に進捗 API は無い。ReadableStream で自前計算:
const res = await fetch(url)
const total = Number(res.headers.get("content-length"))
const reader = res.body.getReader()
let received = 0
const chunks = []
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
received += value.length
console.log(`${(received / total * 100).toFixed(1)}%`)
}
const blob = new Blob(chunks)
Request オブジェクト
URL + 設定をまとめたRequestを作って渡せる。SW では特に重要。
const req = new Request("/api/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ a: 1 }),
})
await fetch(req)
エラーの扱い
fetch は HTTP エラー(4xx/5xx)で reject しない。res.ok を必ずチェック。
ネットワーク失敗・CORS エラー・abort でのみ reject。
try {
const res = await fetch(url)
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`)
}
return await res.json()
} catch (e) {
// ネットワーク or HTTP エラー
}
共通ラッパを作る
async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
const res = await fetch(`/api${path}`, {
...init,
headers: {
"Content-Type": "application/json",
...init.headers,
Authorization: `Bearer ${getToken()}`,
},
})
if (!res.ok) {
const text = await res.text().catch(() => "")
throw new HttpError(res.status, text)
}
if (res.status === 204) return undefined as T
return res.json()
}
class HttpError extends Error {
constructor(public status: number, public body: string) {
super(`HTTP ${status}: ${body}`)
}
}
リトライ
async function fetchWithRetry(url, init = {}, retries = 3, delay = 500) {
for (let i = 0; i <= retries; i++) {
try {
const res = await fetch(url, init)
if (res.ok) return res
if (res.status < 500) return res // 4xx はリトライしない
} catch (e) {
if (i === retries) throw e
}
await new Promise((r) => setTimeout(r, delay * Math.pow(2, i)))
}
}
サーバ側からの fetch
- Node.js: 18+ で標準 fetch、それ以前は
undici/node-fetch - Bun / Deno: 標準
- Cloudflare Workers: 標準(最強)
fetch 代替・拡張
- ky — fetch ラッパ。リトライ・タイムアウト・拡張が楽
- ofetch — Nuxt 由来。エラー / JSON 自動
- axios — XHR ベースの老舗
- Tanstack Query / SWR — fetch ラッパとは別レイヤだが、データ取得の状態管理
よくあるバグ
✗ fetch が成功 = OK と思ってしまう(4xx/5xx はreject されない)
✗ Body を 2 回読もうとする(res.clone())
✗ クロスオリジンで Cookie を送ろうとしてcredentials: "include" 忘れ
✗ FormData + Content-Type を手で指定(boundary が壊れる)
✗ AbortController を作ったが signal を渡し忘れ