- JavaScript
- 技術 💻
JavaScript fetchの使い方|用途別コピペ関数と実行結果
fetchとは、URLを指定してHTTPリクエストを送り、結果を Promise で受け取る JavaScript 標準の関数です。 ブラウザだけでなく Node.js 18 以降でもインストールなしで使え、await fetch(url) でレスポンスを受け取り、await res.json() で中身を取り出す、という2段階が基本です。
この記事では、GET・POSTの基本から、タイムアウト(AbortSignal.timeout / AbortSignal.any)、リトライ、並列実行、ストリーミング、ダウンロード進捗、Cookie、CORSまでを 用途別のコピペ関数 にまとめました。すべて手元に立てたテストサーバーに対して Node.js v22.22.2 と Chromium 141(Playwright)の両方で実行し、出力をそのまま掲載 しています。response.ok を確認しないと 404 でも処理が進む、json() を2回呼ぶとエラーになる、といったつまずきも、実際のエラー文で確認できます。
fetchでやりたいことの早見表
「やりたいこと」から、fetchの書き方と注意点を引けるようにしました。 各行の関数は本文にコピペ用で載せています。
| やりたいこと | 書き方の要点 | 注意点 |
|---|---|---|
| JSONを取得(GET) | const res = await fetch(url); const data = await res.json(); |
res.ok を確認する(404/500 では例外にならない) |
| クエリ文字列を付ける | url.search = new URLSearchParams(params) |
自分で ?q= を連結せずエンコードを任せる |
| JSONを送る(POST) | method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) |
Content-Type がないとサーバーがJSONとして読まないことがある |
| 更新・削除(PUT/DELETE) | method: 'PUT' / 'DELETE' |
204 は本文なし。json() を呼ばない |
| フォーム・ファイル送信 | body: new FormData() |
Content-Type を書かない(境界文字列を自動で付けるため) |
| 認証ヘッダー | headers: { Authorization: 'Bearer ...' } |
401 は res.ok === false で判定 |
| タイムアウト | signal: AbortSignal.timeout(ms) |
エラー名は TimeoutError |
| タイムアウト+キャンセル | signal: AbortSignal.any([ac.signal, AbortSignal.timeout(ms)]) |
ユーザー中断は AbortError |
| 失敗時に再試行 | 5xx・429・通信エラーだけ指数バックオフで再送 | 4xx を再送しても結果は同じ |
| 複数を同時に取得 | Promise.all / 失敗も集めるなら Promise.allSettled |
all は1つ失敗で全体が失敗 |
| 少しずつ読む | res.body.pipeThrough(new TextDecoderStream()).getReader() |
行の途中で分割されるのでバッファする |
| ダウンロード進捗 | Content-Length と reader.read() の合計で計算 |
圧縮転送だと合計と一致しないことがある |
| Cookieを送る | 同じオリジンは既定で送る。別オリジンは credentials: 'include' |
別オリジンはサーバー側の許可も必要 |
| 別オリジンのAPIを呼ぶ | サーバーが Access-Control-Allow-Origin を返す |
CORSはブラウザだけの制限(Nodeは対象外) |
fetchのオプション早見
| オプション | よく使う値 | 意味 |
|---|---|---|
method |
'GET'(既定)/ 'POST' / 'PUT' / 'PATCH' / 'DELETE' |
HTTPメソッド |
headers |
{ 'Content-Type': 'application/json' } |
送るヘッダー |
body |
文字列 / FormData / URLSearchParams / Blob |
送る本文(GETでは指定不可) |
signal |
AbortSignal.timeout(5000) |
中断用の合図 |
credentials |
'same-origin'(既定)/ 'include' / 'omit' |
Cookieを送るか |
mode |
'cors'(既定)/ 'same-origin' / 'no-cors' |
別オリジンへの送り方 |
cache |
'no-store' / 'reload' など |
ブラウザのHTTPキャッシュの使い方 |
Responseでよく使うもの
| プロパティ・メソッド | 返すもの |
|---|---|
res.ok |
ステータスが 200〜299 なら true |
res.status / res.statusText |
404 / 'Not Found' |
res.headers.get('Content-Type') |
ヘッダーの値 |
res.json() / res.text() / res.blob() / res.arrayBuffer() / res.formData() |
本文(どれか1回だけ) |
res.body |
ReadableStream(少しずつ読む) |
res.clone() |
本文を2回読みたいときの複製 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScript fetchの使い方|用途別コピペ関数と実行結果」
検証環境
| 項目 | バージョン・内容 |
|---|---|
| Node.js | v22.22.2(fetch は標準搭載) |
| ブラウザ | Chromium 141.0.7390.37(Playwright 1.56.0) |
| テストサーバー | Node の http で作成。localhost:7150(API)と localhost:7151(別オリジン確認用) |
| 比較用 | axios 1.20.0 |
以下の関数は1つのファイル(lib.mjs)にまとめ、Node では import、ブラウザでは import('/lib.mjs') で読み込んで同じコードを実行しました。Node とブラウザで結果が違ったものは、両方の出力を載せています。
fetchの基本:GETでJSONを取得する
fetchは「レスポンスが届いた時点」と「本文を読み終えた時点」の2回待ちます。 1回目の await fetch() ではステータスとヘッダーだけが分かり、本文は await res.json() で読み出します。
// 共通:ステータスを確認してJSONを返す
export async function getJSON(url, options = {}) {
const res = await fetch(url, options);
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
return res.json();
}
const data = await getJSON('http://localhost:7150/api/items');
console.log(data);
実行結果(Node v22.22.2/Chromium 141 とも同じ):
{"query":{},"items":[{"id":1,"name":"ノート"},{"id":2,"name":"ペン"}]}
.then() でつなぐ書き方もありますが、エラー処理を try/catch でまとめて書ける async/await が読みやすいので、この記事ではすべて await で書いています。
よくある落とし穴(実際のエラー文つき)
404 や 500 でも例外にならない(response.ok を見る)
fetchが例外を投げるのは「通信そのものが失敗したとき」だけで、404 や 500 は「正常に届いたレスポンス」として扱われます。 存在しないURLを res.ok を見ずに処理すると、エラー用のJSONをそのまま正常データとして受け取ります。
// 直す前:ok を見ない
const res = await fetch('http://localhost:7150/api/missing');
const data = await res.json();
console.log({ ok: res.ok, status: res.status, data });
{"ok":false,"status":404,"data":{"error":"not found"}}
// 直した後:getJSON(上で定義)を使う
try {
await getJSON('http://localhost:7150/api/missing');
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
Error: HTTP 404 Not Found
fetchで起きる失敗の種類と見分け方
| 状況 | fetchの結果 | 実際のエラー(Node 22) | 実際のエラー(Chromium 141) |
|---|---|---|---|
| 404 / 500 | 成功(res.ok === false) |
例外なし | 例外なし |
| サーバーに接続できない | 例外 | TypeError: fetch failed(cause.code は ECONNREFUSED) |
TypeError: Failed to fetch |
| CORSで拒否 | 例外 | (Nodeでは発生しない) | TypeError: Failed to fetch |
| タイムアウト | 例外 | TimeoutError |
TimeoutError |
JSONでない本文を json() |
json() が例外 |
SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON |
同じ |
Node の fetch failed は詳細が err.cause に入るので、ログには err.cause?.code も出すと原因が分かりやすくなります。
json() を2回呼ぶとエラーになる
レスポンスの本文はストリームなので、一度しか読めません。
const res = await fetch('http://localhost:7150/api/items');
await res.json();
await res.json(); // 2回目
| 環境 | 実際のエラー |
|---|---|
| Node v22.22.2 | TypeError: Body is unusable: Body has already been read |
| Chromium 141 | TypeError: Failed to execute 'json' on 'Response': body stream already read |
ログ用とデータ用に2回読みたいときは、先に res.clone() で複製します。
const res = await fetch('http://localhost:7150/api/items');
const copy = res.clone();
const data = await res.json(); // データとして使う
const raw = await copy.text(); // ログに残す
HTMLが返ってきて json() が失敗する
メンテナンス画面などのHTMLに json() を使うと、SyntaxError: Unexpected token '<' になります。 先に res.headers.get('Content-Type') が application/json を含むか確かめ、違えば await res.text() の先頭をログに出すと原因が分かりやすくなります。
用途別コピペ関数集
クエリ文字列を付けてGETする(URLSearchParams)
URL と URLSearchParams に組み立てを任せると、日本語や & も正しくエンコードされます。
export async function searchItems(base, params) {
const url = new URL('/api/items', base);
url.search = new URLSearchParams(params).toString();
return getJSON(url);
}
await searchItems('http://localhost:7150', { q: 'ノート ペン', page: 2, tag: 'a&b' });
組み立てられたURL(Node v22.22.2):
http://localhost:7150/api/items?q=%E3%83%8E%E3%83%BC%E3%83%88+%E3%83%9A%E3%83%B3&page=2&tag=a%26b
サーバーが受け取った値:
{"q":"ノート ペン","page":"2","tag":"a&b"}
tag=a&b を文字列連結で書くと tag=a と b に分かれてしまいますが、URLSearchParams なら a%26b とエンコードされ、そのまま届きました。数値の 2 は文字列 "2" として届く点も覚えておくと便利です。
JSONをPOSTする
export async function postJSON(url, data) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
await postJSON('http://localhost:7150/api/items', { name: '付箋', qty: 3 });
実行結果(Node/Chromium とも同じ):
{"received":{"name":"付箋","qty":3},"contentType":"application/json"}
body にはオブジェクトではなく JSON.stringify() した文字列を渡します。
PUTで更新、DELETEで削除する
export async function putJSON(url, data) {
const res = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export async function deleteItem(url) {
const res = await fetch(url, { method: 'DELETE' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.status; // 204 No Content は本文なし → json() を呼ばない
}
PUT → {"id":"1","updated":{"qty":5}}
DELETE → 204
204 No Content に json() を呼ぶと本文が空なので SyntaxError になります(Node v22.22.2 では SyntaxError: Unexpected end of JSON input、Chromium 141 では SyntaxError: Failed to execute 'json' on 'Response': Unexpected end of JSON input)。そのため削除APIではステータスだけを返しています。
フォームやファイルを送る(FormData)
FormData を body に渡すと、multipart/form-data と区切り文字(boundary)が自動で付きます。自分で Content-Type を書くと区切り文字が抜けるので、書かないのがポイントです。
試しに headers: { 'Content-Type': 'multipart/form-data' } を付けて同じ FormData を送ると、テストサーバー(Node の Response.formData() で解析)は TypeError: Failed to parse body as FormData. となり、400 を返しました。
export async function upload(url, file, title) {
const fd = new FormData();
fd.append('title', title);
fd.append('file', file, file.name ?? 'memo.txt');
const res = await fetch(url, { method: 'POST', body: fd });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// ブラウザなら <input type="file"> の files[0] をそのまま渡せます
const file = new File(['在庫メモ'], 'memo.txt', { type: 'text/plain' });
await upload('http://localhost:7150/api/upload', file, '棚卸し');
実行結果(Node/Chromium とも同じ):
{"contentType":"multipart/form-data","fields":{"title":"棚卸し","file":{"name":"memo.txt","type":"text/plain","size":12}}}
size が 12 なのは、「在庫メモ」の4文字がUTF-8で各3バイトだからです。ファイルなしのフォーム(application/x-www-form-urlencoded)を送るなら、body: new URLSearchParams({ name: '付箋' }) と書けます。
認証ヘッダーを付ける
const data = await getJSON('http://localhost:7150/api/me', {
headers: { Authorization: 'Bearer demo-token' },
});
付けた場合 → {"user":"demo"}
付けない場合 → Error: HTTP 401 Unauthorized
トークンはソースコードに直接書かず、ログイン後に受け取った値や環境変数から渡します。
タイムアウトを付ける(AbortSignal.timeout)
fetchにはタイムアウトのオプションがないため、signal に AbortSignal.timeout(ミリ秒) を渡します。 以前は AbortController と setTimeout を組み合わせていましたが、今は1行で書けます。
export async function getWithTimeout(url, ms) {
try {
return await getJSON(url, { signal: AbortSignal.timeout(ms) });
} catch (err) {
if (err.name === 'TimeoutError') return `タイムアウト(${ms}ms): ${err.message}`;
if (err.name === 'AbortError') return `中断: ${err.message}`;
throw err;
}
}
// 3秒かかるAPIに1秒のタイムアウト
await getWithTimeout('http://localhost:7150/api/slow?ms=3000', 1000);
| 環境 | 実行結果 |
|---|---|
| Node v22.22.2 | タイムアウト(1000ms): The operation was aborted due to timeout(経過 1002ms) |
| Chromium 141 | タイムアウト(1000ms): signal timed out |
メッセージは環境で違うので、判定は err.name === 'TimeoutError' で行うと両方で動きます。
タイムアウトと「キャンセルボタン」を両立する(AbortSignal.any)
AbortSignal.any([...]) は、渡した合図のどれかが中断されたら中断される新しい合図を作ります。 検索のキャンセルボタンとタイムアウトを同時に効かせたいときに使います(Chrome 116、Firefox 124、Safari 17.4、Node 20.3 以降)。
export async function getCancelable(url, userSignal, ms) {
const signal = AbortSignal.any([userSignal, AbortSignal.timeout(ms)]);
try {
return await getJSON(url, { signal });
} catch (err) {
return `${err.name}: ${err.message}`;
}
}
const ac = new AbortController();
cancelButton.onclick = () => ac.abort(); // 検証では 300ms 後に abort() を呼んだ
await getCancelable('http://localhost:7150/api/slow?ms=3000', ac.signal, 1000);
| 状況 | Node v22.22.2 | Chromium 141 |
|---|---|---|
| 300ms でユーザーが中断 | AbortError: This operation was aborted |
AbortError: signal is aborted without reason |
| 500ms でタイムアウト | TimeoutError: The operation was aborted due to timeout |
TimeoutError: signal timed out |
err.name で「ユーザーが止めた(AbortError)」と「時間切れ(TimeoutError)」を区別でき、画面の表示を分けられます。
失敗したら再試行する(指数バックオフ)
再試行するのは、時間をおけば成功する見込みがあるもの(通信エラー、5xx、429 Too Many Requests)だけにします。 待ち時間を 200→400→800ms と倍にしていく方法を指数バックオフといい、少しゆらぎ(ランダムな加算)を足すと、多数のクライアントが同時に再送するのを避けられます。
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function fetchWithRetry(url, { retries = 3, baseDelay = 200, log = () => {} } = {}) {
for (let attempt = 0; ; attempt++) {
let res;
try {
res = await fetch(url);
} catch (err) {
// ネットワークエラー(接続できない等)は再試行の対象
if (attempt >= retries) throw err;
log(`${err.name} → 再試行`);
}
if (res) {
if (res.ok) return res.json();
// 5xx と 429 だけ再試行。それ以外の 4xx は送り直しても同じ結果
const retryable = res.status >= 500 || res.status === 429;
if (!retryable || attempt >= retries) throw new Error(`HTTP ${res.status}`);
log(`HTTP ${res.status} → ${baseDelay * 2 ** attempt}ms 待って再試行`);
}
await sleep(baseDelay * 2 ** attempt + Math.random() * 100); // 200, 400, 800ms… +ゆらぎ
}
}
2回目まで 503 を返し、3回目で成功するAPIに対して実行した結果(Node/Chromium とも同じ):
HTTP 503 → 200ms 待って再試行
HTTP 503 → 400ms 待って再試行
{"ok":true,"attempt":3}
404 のAPIに対しては再試行せず、すぐに HTTP 404 で失敗しました。POST を再試行する場合は、サーバー側で同じ注文が二重に登録されない仕組み(冪等性キーなど)があるか確認してからにします。
複数のAPIを同時に呼ぶ(Promise.all / allSettled)
順番に await すると待ち時間が足し算になりますが、Promise.all で同時に投げると最も遅いものの時間で済みます。
const results = await Promise.all(
[1000, 1000, 1000].map((ms) => getJSON(`http://localhost:7150/api/slow?ms=${ms}`)),
);
1秒かかるAPI×3 → 合計 1003ms(順番に呼ぶと約3秒)
1つでも失敗すると Promise.all 全体が失敗します。 成功したものだけでも使いたいときは Promise.allSettled を使います。
const settled = await Promise.allSettled([
getJSON('http://localhost:7150/api/items'),
getJSON('http://localhost:7150/api/missing'),
]);
console.log(settled.map((r) => r.status === 'fulfilled' ? r.status : `${r.status}: ${r.reason.message}`));
[ 'fulfilled', 'rejected: HTTP 404 Not Found' ]
届いた分から少しずつ読む(ストリーミング)
res.body は ReadableStream なので、全部届くのを待たずに読み進められます。 1行1JSONの形式(NDJSON)を、200ms ごとに1行送るサーバーで試しました。
export async function readNDJSON(url, onLine) {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buf = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += value;
const lines = buf.split('\n');
buf = lines.pop(); // 行の途中で切れた残りは次回に回す
for (const line of lines) if (line) onLine(JSON.parse(line));
}
}
Node v22.22.2 : step1 +201ms, step2 +400ms, step3 +601ms
Chromium 141 : step1 +202ms, step2 +402ms, step3 +602ms
サーバーが送った間隔のとおりに1行ずつ受け取れています。 res.json() を使うと、最後の行が届く約600ms後まで何も表示できません。
ダウンロードの進捗を表示する
export async function downloadWithProgress(url, onProgress) {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const total = Number(res.headers.get('Content-Length')) || 0;
const reader = res.body.getReader();
const chunks = [];
let received = 0;
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
onProgress(received, total);
}
return new Blob(chunks);
}
await downloadWithProgress('http://localhost:7150/api/download', (r, t) => {
console.log(`${Math.round((r / t) * 100)}%`);
});
1MBのファイル(256KBずつ送信)での結果:
| 環境 | 進捗の呼び出し回数 | 最後の値 | Blobのサイズ |
|---|---|---|---|
| Node v22.22.2 | 20回(6%, 12%, 19%, 25% … 100%) | 100% | 1,048,576 |
| Chromium 141 | 8回 | 100% | 1,048,576 |
1回に届く量(チャンクの大きさ)は環境によって変わるので、進捗の刻みは保証されません。また、gzip などで圧縮して送られると Content-Length は圧縮後の大きさになり、合計と一致しないことがあります。なお、fetch にはアップロードの進捗を取る標準的な方法がないため、送信の進捗バーが必要な場合は XMLHttpRequest の upload.onprogress を使います。
Cookieを送る(credentials)
ブラウザでは、同じオリジン(プロトコル・ホスト・ポートが同じ)へのリクエストには既定で Cookie が送られます。
await fetch('/api/login'); // Set-Cookie: sid=abc123; HttpOnly
await (await fetch('/api/whoami')).json(); // 既定(same-origin)
await (await fetch('/api/whoami', { credentials: 'omit' })).json(); // 送らない
| 環境・設定 | サーバーが受け取った Cookie |
|---|---|
Chromium:既定(same-origin) |
sid=abc123 |
Chromium:credentials: 'omit' |
null |
Chromium:res.headers.get('set-cookie') |
null(JavaScriptからは読めない) |
Node:res.headers.getSetCookie() |
['sid=abc123; Path=/; HttpOnly; SameSite=Lax'] |
| Node:次のリクエスト | null(Node の fetch は Cookie を自動保存しない) |
別オリジンに Cookie を送るには credentials: 'include' が必要で、サーバー側も Access-Control-Allow-Credentials: true と、* ではない具体的な Access-Control-Allow-Origin を返す必要があります。 Node で Cookie を続けて送りたいときは、受け取った値を自分で Cookie ヘッダーに入れます。
CORSエラーを実際に出して理解する
CORS(オリジン間リソース共有)は、ブラウザが「別のオリジンのAPIの結果を、そのページのJavaScriptに渡してよいか」をサーバーの応答ヘッダーで確認する仕組みです。 localhost:7151 のページから localhost:7150 のAPIを呼んで確かめました(ポートが違うだけで別オリジンです)。
// http://localhost:7151 のページで実行
await fetch('http://localhost:7150/api/nocors'); // 許可ヘッダーなし
await fetch('http://localhost:7150/api/cors'); // Access-Control-Allow-Origin: http://localhost:7151
| 呼び出し | Chromium 141 の結果 |
|---|---|
/api/nocors(許可ヘッダーなし) |
TypeError: Failed to fetch |
/api/cors(許可あり) |
{"ok":true,"origin":"http://localhost:7151"} |
/api/cors に JSON を POST(事前確認 OPTIONS に許可を返す) |
ステータス 200 |
/api/nocors に mode: 'no-cors' |
type: "opaque", status: 0, ok: false(中身は読めない) |
Node から /api/nocors |
{"secret":"same-origin only"}(Node にはCORSの制限がない) |
ブラウザの開発者ツールのコンソールには、次のメッセージが出ました。
Access to fetch at 'http://localhost:7150/api/nocors' from origin 'http://localhost:7151' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
JavaScript側の例外は Failed to fetch だけで、理由はコンソールにしか出ません。 CORSの対処はサーバー側で行います。
| 方法 | 内容 |
|---|---|
| APIサーバーで許可する | Access-Control-Allow-Origin: https://自分のサイト を返す。JSONのPOSTなどは OPTIONS(事前確認)にも Access-Control-Allow-Methods / Allow-Headers を返す |
| 同じオリジンにする | 開発中は Vite などの開発サーバーのプロキシ機能で /api を転送する |
| サーバー経由にする | 自分のサーバー(Node など)からAPIを呼ぶ。サーバー間通信にCORSは関係しない |
mode: 'no-cors' は「エラーを消す設定」ではなく「中身を読めない opaque レスポンスを受け取る設定」なので、データ取得には使えません。
fetchとaxiosの違い
axios 1.20.0 で同じAPIを呼んだ結果です。
import axios from 'axios';
const r = await axios.get('http://localhost:7150/api/items', { params: { q: 'ノート', page: 2 } });
console.log(r.status, r.data.query);
try { await axios.get('http://localhost:7150/api/missing'); }
catch (e) { console.log(e.name, e.message, e.response.status); }
try { await axios.get('http://localhost:7150/api/slow?ms=3000', { timeout: 500 }); }
catch (e) { console.log(e.code, e.message); }
200 { q: 'ノート', page: '2' }
AxiosError Request failed with status code 404 404
ECONNABORTED timeout of 500ms exceeded
| 項目 | fetch | axios |
|---|---|---|
| 導入 | 不要(ブラウザ・Node 18+ 標準) | npm install axios |
| 404/500 のとき | 例外にならない(res.ok で判定) |
例外になる(e.response.status) |
| JSONの取り出し | await res.json() |
自動で r.data |
| JSONの送信 | JSON.stringify と Content-Type を自分で |
オブジェクトを渡せば自動 |
| クエリ文字列 | URLSearchParams |
params オプション |
| タイムアウト | signal: AbortSignal.timeout(ms) |
timeout: ms |
| キャンセル | AbortController |
signal(AbortController 対応) |
| 共通処理(インターセプター) | 関数でラップして作る | interceptors が標準 |
| ダウンロード進捗 | res.body を読んで計算 |
onDownloadProgress |
| アップロード進捗 | 標準の方法なし | onUploadProgress |
| ストリーミング | res.body(ReadableStream)が標準 |
環境により設定が必要 |
依存を増やさず小さく書くなら fetch、共通のエラー処理やアップロード進捗をまとめて使いたいなら axios が選びやすい基準です。fetch でも、この記事の getJSON のような薄いラッパーを1つ用意すれば、多くの場面で同じように書けます。
よくある質問
Q. fetchで404なのに catch に入らないのはなぜですか?
A. fetchは「レスポンスが返ってきた」時点で成功扱いにし、HTTPのステータスでは失敗にしない仕様だからです。if (!res.ok) throw new Error(...) を入れて、自分で例外にします。実行結果でも、404 のとき res.ok は false、res.status は 404 でした。
Q. Node.jsでfetchを使うのにライブラリは必要ですか?
A. Node.js 18 以降は標準で使えます。 この記事の検証も Node v22.22.2 で追加パッケージなしに実行しました。ブラウザとの違いは、CORSの制限がないこと、Cookieを自動で保存しないこと、エラーの詳細が err.cause に入ることなどです。
Q. タイムアウトを設定するにはどうすればいいですか?
A. fetch(url, { signal: AbortSignal.timeout(5000) }) のように書きます。 時間切れのときは err.name が TimeoutError になります。キャンセルボタンと両立したい場合は AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]) を使います。
Q. Failed to fetch と出たときは何を確認すればいいですか?
A. ブラウザでは、接続できない場合もCORSで拒否された場合も同じ TypeError: Failed to fetch になります。開発者ツールのコンソールに blocked by CORS policy が出ていればCORS、ERR_CONNECTION_REFUSED ならサーバーが起動していないかURLの誤り、と見分けられます。
まとめ
- fetchは
await fetch()とawait res.json()の2段階。Node 18 以降とブラウザで同じコードが動く。 - 404/500 は例外にならないので
res.okを必ず確認し、ラッパー関数(getJSON)にまとめる。 - 本文は1回しか読めない。2回読むなら
res.clone()。 - タイムアウトは
AbortSignal.timeout、キャンセルと両立するならAbortSignal.any。判定はerr.nameで。 - FormData では
Content-Typeを書かない。クエリ文字列はURLSearchParamsに任せる。 - 再試行は通信エラー・5xx・429 だけ、指数バックオフで。並列は
Promise.all/失敗も集めるならallSettled。 - CORSはブラウザだけの仕組みで、対処はサーバー側。理由はコンソールに出る。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScript fetchの使い方|用途別コピペ関数と実行結果」
株式会社RJCでは、APIとの通信のような毎日使う技術も、実際に動かして確かめた知見をプロジェクトの仲間と共有しています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。