



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp









生徒APIからデータを取ってきて画面に出したいんですが、fetchの書き方がいまいち掴めません…。
ペン博士fetchは「投げる・待つ・受け取る」の3段階で考えると簡単だよ。async/awaitで書けば見た目もほぼ同期処理と同じになる。落とし穴のエラー処理まで順番に見ていこう。
外部のAPIからデータを取得する、フォームの内容をサーバーへ送る、ページを再読み込みせずに一覧を更新する。こうした処理の入口がfetch APIです。
かつてはXMLHttpRequestという長い記述が必要でしたが、fetchなら数行で書けます。ブラウザに標準搭載されているため、ライブラリの読み込みも不要です。
ただし1つだけ有名な落とし穴があります。404や500が返ってきてもcatchに入らないという挙動です。この記事では基本の書き方から、その対処、実務で必要になるエラー処理・タイムアウト・並列取得までまとめて解説します。

fetchは「リクエストを投げる」「レスポンスを受け取る」「中身を取り出す」の3段階で動きます。
// 最小の書き方
fetch('https://api.example.com/posts')
.then(response => response.json()) // 中身をJSONとして取り出す
.then(data => console.log(data)) // 取り出した結果を使う
.catch(error => console.error(error));| 段階 | 何をしている | 戻り値 |
|---|---|---|
fetch(url) | リクエストを送る | Responseを含むPromise |
response.json() | 本文をJSONに変換 | データを含むPromise |
data | 実際の値を使う | オブジェクトや配列 |
重要なのは、fetchが返すのがデータそのものではなくResponseオブジェクトだという点です。ステータスコードやヘッダーの情報が入っており、本文は.json()などで別途取り出します。
.then()の連鎖は処理が増えると読みにくくなります。実務ではasync/awaitで書くのが主流です。
async function getPosts() {
try {
const response = await fetch('https://api.example.com/posts');
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error('取得に失敗しました:', error);
}
}
getPosts();awaitは「この行が終わるまで待つ」という意味です。上から下へ順に読めるため、処理の流れがそのままコードの並びになります。
注意点として、awaitはasync関数の中でしか使えません。関数の外で使うとエラーになります(モジュール直下のトップレベルawaitは例外)。

fetch最大の落とし穴です。サーバーが404や500を返しても、通信自体は成功しているためcatchに入りません。
// 危険な書き方:404でもエラーにならない
async function bad() {
try {
const res = await fetch('https://api.example.com/not-found'); // 404
const data = await res.json(); // ここでJSON解析エラーになることも
console.log(data);
} catch (e) {
console.error(e); // 期待した場所で捕まらない
}
}catchに入るのは、ネットワーク断・CORS違反・URLが不正といった通信自体が失敗した場合だけです。HTTPステータスは自分で判定します。
// 正しい書き方:res.ok を必ず確認する
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
if (!res.ok) {
// 200〜299以外はここで弾く
throw new Error(`HTTPエラー: ${res.status} ${res.statusText}`);
}
return await res.json();
}| 状況 | catchに入る? | 判定方法 |
|---|---|---|
| 通信断・オフライン | 入る | catchで捕まえる |
| CORS違反 | 入る | catchで捕まえる |
| 404 Not Found | 入らない | res.okで判定 |
| 500 サーバーエラー | 入らない | res.okで判定 |
| JSONの形式が不正 | 入る | .json()で例外 |
res.okはステータスが200〜299のときtrueになる便利なプロパティです。fetchを書いたら必ずこの判定を入れる、と覚えてください。

最もよく使う形です。取得したデータをDOMに反映するところまで通しで書きます。
async function renderPosts() {
const list = document.querySelector('#post-list');
list.textContent = '読み込み中…';
try {
const res = await fetch('https://api.example.com/posts?limit=10');
if (!res.ok) throw new Error(`HTTPエラー: ${res.status}`);
const posts = await res.json();
// 一度に組み立ててから差し込む(描画回数を減らす)
list.innerHTML = posts
.map(post => `<li><a href="${post.url}">${post.title}</a></li>`)
.join('');
} catch (error) {
list.textContent = 'データを取得できませんでした。時間をおいてお試しください。';
console.error(error);
}
}
document.addEventListener('DOMContentLoaded', renderPosts);クエリパラメータが多い場合は、文字列を手で連結せずURLSearchParamsを使うと安全です。
const params = new URLSearchParams({
category: 'css',
limit: 10,
keyword: '中央寄せ', // 日本語も自動でエンコードされる
});
const res = await fetch(`https://api.example.com/posts?${params}`);
送信では、メソッド・ヘッダー・本文の3つを指定します。
async function sendContact(formData) {
const res = await fetch('https://api.example.com/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: formData.name,
email: formData.email,
message: formData.message,
}),
});
if (!res.ok) throw new Error(`送信に失敗しました: ${res.status}`);
return await res.json();
}bodyには文字列を渡す必要があります。オブジェクトをそのまま渡しても送信されないため、JSON.stringify()で変換します。
フォームの値をまとめて送る場合は、FormDataを使うと入力欄を1つずつ拾う必要がなくなります。
const form = document.querySelector('#contact-form');
form.addEventListener('submit', async (event) => {
event.preventDefault(); // ページ遷移を止める
const res = await fetch('/api/contact', {
method: 'POST',
body: new FormData(form), // Content-Typeは自動で付く
});
if (res.ok) {
form.reset();
alert('送信しました');
}
});FormDataを使うときはContent-Typeを自分で書きません。境界文字列を含むヘッダーをブラウザが自動生成するため、手で指定すると壊れます。
レスポンスの取り出し方は、返ってくる形式によって変えます。
| メソッド | 戻り値 | 使う場面 |
|---|---|---|
.json() | オブジェクト・配列 | API全般 |
.text() | 文字列 | HTML・CSV・プレーンテキスト |
.blob() | バイナリ | 画像・PDFのダウンロード |
.formData() | FormData | フォーム形式の応答 |
.arrayBuffer() | 生バイト列 | 音声・独自形式 |
// テキストとして取得
const html = await (await fetch('/parts/header.html')).text();
// 画像を取得して表示する
const res = await fetch('/img/photo.jpg');
const blob = await res.blob();
document.querySelector('#preview').src = URL.createObjectURL(blob);注意点として、レスポンスの本文は一度しか読み取れません。.json()を呼んだあとに.text()を呼ぶとエラーになります。両方必要ならres.clone()で複製してください。

fetchには標準のタイムアウト設定がありません。応答が返らないと待ち続けるため、自分で打ち切ります。
async function fetchWithTimeout(url, options = {}, timeout = 8000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, { ...options, signal: controller.signal });
if (!res.ok) throw new Error(`HTTPエラー: ${res.status}`);
return await res.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('通信がタイムアウトしました');
}
throw error;
} finally {
clearTimeout(timer); // 成功しても必ず解除する
}
}AbortControllerは中断にも使えます。検索の入力途中で古いリクエストを打ち切る、といった処理が代表例です。
let currentController = null;
async function search(keyword) {
currentController?.abort(); // 前の検索を中断
currentController = new AbortController();
const res = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`, {
signal: currentController.signal,
});
return await res.json();
}複数のAPIを叩く場合、順番にawaitすると待ち時間が積み重なります。同時に投げれば最も遅い1本の時間で済みます。
// 遅い書き方:合計3秒かかる
const a = await fetch('/api/a'); // 1秒
const b = await fetch('/api/b'); // 1秒
const c = await fetch('/api/c'); // 1秒
// 速い書き方:最も遅い1本ぶん(約1秒)
const [resA, resB, resC] = await Promise.all([
fetch('/api/a'),
fetch('/api/b'),
fetch('/api/c'),
]);Promise.allは1つでも失敗すると全体が失敗します。一部が失敗しても続行したい場合はPromise.allSettledを使います。
const results = await Promise.allSettled([
fetch('/api/a').then(r => r.json()),
fetch('/api/b').then(r => r.json()),
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`${i}番目 成功`, result.value);
} else {
console.warn(`${i}番目 失敗`, result.reason);
}
});
fetchでよく遭遇するのがCORSエラーです。「別のドメインへのアクセスを、相手サーバーが許可していない」という意味になります。
Access to fetch at 'https://api.example.com/data' from origin
'https://mysite.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.よく見かけるmode: 'no-cors'は解決策ではありません。エラーは消えますが、レスポンスの中身が読めない状態(opaque)になるだけです。
// これはCORSの解決策ではない
const res = await fetch(url, { mode: 'no-cors' });
console.log(res.status); // 常に 0。中身も読めないなお、APIキーやトークンをブラウザのJavaScriptに書くと、閲覧者に丸見えになります。認証が必要なAPIはサーバー側から呼び出してください。

ここまでの内容をまとめた、そのまま使える関数です。エラー処理・タイムアウト・JSON判定を含みます。
/**
* fetchのラッパー。HTTPエラー・タイムアウトを例外として扱う。
* @param {string} url
* @param {object} options fetchのオプション
* @param {number} timeout ミリ秒。既定8秒
*/
async function request(url, options = {}, timeout = 8000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, { ...options, signal: controller.signal });
if (!res.ok) {
// サーバーがエラー詳細を返す場合に備えて本文も読む
let detail = '';
try {
detail = await res.text();
} catch (_) { /* 読めなくても続行 */ }
throw new Error(`HTTP ${res.status} ${res.statusText} ${detail.slice(0, 120)}`);
}
// 本文が無い応答(204など)に備える
if (res.status === 204) return null;
const type = res.headers.get('content-type') || '';
return type.includes('application/json') ? await res.json() : await res.text();
} catch (error) {
if (error.name === 'AbortError') throw new Error('通信がタイムアウトしました');
throw error;
} finally {
clearTimeout(timer);
}
}
// 使い方
const posts = await request('/api/posts');
const created = await request('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: '新しい記事' }),
});この関数を1つ用意しておけば、各所でres.okの判定を書き忘れる事故がなくなります。
fetchが期待通りに動かない場合、次の順で確認すると原因にたどり着けます。
| 症状 | 原因 | 確認・対処 |
|---|---|---|
| catchに入らないのに失敗 | HTTPエラー | res.okを判定する |
[object Object]が送られる | bodyが文字列でない | JSON.stringifyする |
| CORSエラー | サーバー側の許可なし | API提供元かプロキシで対応 |
| awaitで構文エラー | async関数の外 | 関数にasyncを付ける |
| 本文が2回読めない | 既に消費済み | res.clone()を使う |
| ずっと待ち続ける | タイムアウト未設定 | AbortControllerを使う |
| データがundefined | awaitの付け忘れ | .json()にもawaitを付ける |
最後の項目は初学者に多いミスです。.json()自体もPromiseを返すため、awaitを2回書くのが正しい形になります。
WordPressサイトでfetchを使うときは、REST APIと認証まわりに固有の作法があります。
// 公開記事の取得は認証不要
const res = await fetch('/wp-json/wp/v2/posts?per_page=5&_fields=id,title,link');
const posts = await res.json();
document.querySelector('#latest').innerHTML = posts
.map(p => `<li><a href="${p.link}">${p.title.rendered}</a></li>`)
.join('');_fieldsで必要な項目だけ指定すると、レスポンスが数分の一に軽くなります。記事本文まで含めると1件で数十KBになるため、一覧表示では必ず絞ってください。
// 投稿の更新など、書き込み系はnonceを添える
const res = await fetch('/wp-json/wp/v2/posts/123', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': window.wpApiSettings.nonce, // wp_localize_scriptで渡す
},
credentials: 'same-origin', // ログインCookieを送る
body: JSON.stringify({ title: '新しいタイトル' }),
});credentials: 'same-origin'を忘れるとログイン状態が伝わらず、401が返ります。nonceは有効期限が短いため、長時間開いたままの画面では再取得が必要です。
<?php
// functions.php 側でnonceをJSに渡す
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script( 'my-app', get_stylesheet_directory_uri() . '/app.js', [], null, true );
wp_localize_script( 'my-app', 'wpApiSettings', [
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
] );
} );この3行を書いておけば、JavaScript側から安全にREST APIを呼べます。APIキーのようにソースへ直書きする必要がありません。
fetchはページ表示後に走るため、書き方によっては読者を待たせます。3点だけ意識してください。
const list = document.querySelector('#post-list');
// 高さを保った状態で読み込み中を表示する
list.style.minHeight = '240px';
list.innerHTML = '<li class="loading">読み込み中…</li>';
try {
const posts = await request('/wp-json/wp/v2/posts?per_page=5');
list.innerHTML = posts.map(p => `<li>${p.title.rendered}</li>`).join('');
} catch (e) {
list.innerHTML = '<li class="error">読み込みに失敗しました。再読み込みしてください。</li>';
} finally {
list.style.minHeight = '';
}min-heightで先に場所を取っておくと、データが届いた瞬間に下の要素が押し下げられる現象を防げます。Core Web VitalsのCLSに直結する配慮です。
| 対策 | 効果 | 手間 |
|---|---|---|
| 読み込み中の表示 | 離脱を減らす | 1行 |
| エラー文言 | 不信感を防ぐ | 1行 |
| min-heightで高さ確保 | CLSを防ぐ | 2行 |
| _fieldsで項目を絞る | 通信量を削減 | URLに追記 |
どれも数行で済みますが、体感速度と信頼感に効きます。fetchを書いたらセットで入れる習慣にしてください。
fetchは「リクエストを投げる」「Responseを受け取る」「本文を取り出す」の3段階で動きます。async/awaitで書けば上から順に読めるコードになり、実務ではこちらが主流です。最大の注意点は404や500ではcatchに入らないことで、必ずres.okで判定して自分で例外を投げます。POSTではbodyをJSON.stringifyで文字列にし、FormDataを使う場合はContent-Typeを自分で指定しないこと。タイムアウトは標準にないためAbortControllerで実装し、複数取得はPromise.allで並列化します。CORSエラーはサーバー側の設定問題で、mode: 'no-cors'では解決しません。
fetchは通信が成立した時点で成功とみなす仕様のためです。404や500はサーバーが正常に応答した結果なので、Promiseは解決されます。catchに入るのはネットワーク断・CORS違反・URLが不正な場合だけです。if (!res.ok) throw new Error(...)を必ず書いてください。
asyncを付けた関数の中でのみ使えます。fetch()と、本文を取り出す.json()の両方がPromiseを返すため、awaitは2回必要です。付け忘れると、データではなくPromiseオブジェクトが変数に入り、undefinedとして扱われる原因になります。
bodyにオブジェクトをそのまま渡していないか確認してください。JSON.stringify()で文字列に変換し、Content-Type: application/jsonを指定する必要があります。逆にFormDataを使う場合は、Content-Typeを手で書くと壊れるため指定しません。
できません。ブラウザがアクセス元を制限する仕組みで、許可を出せるのはAPI提供側のサーバーだけです。自社APIならAccess-Control-Allow-Originを設定し、外部APIなら自分のサーバーを経由させます。mode: 'no-cors'はエラーを消すだけで、応答の中身は読めません。
fetchで十分なケースがほとんどです。ライブラリはタイムアウトやエラー処理が最初から備わっている利点がありますが、この記事の汎用関数のようなラッパーを1つ作れば同等の使い勝手になります。読み込むファイルを増やさずに済むぶん、表示速度の面でも有利です。

目的に合わせて選べる「AI副業」「AI転職」「AI活用」の3コースを用意。副収入・キャリアチェンジ・日常の生産性アップまで、あなたのゴールに合わせてAIを学べます。
会員登録はカンタン30秒で完了します。まずは公式LINEから、無料でAI学習をスタートしましょう!
WithCodeでWeb制作を習得後、フリーランスエンジニアとして活動。HTML/CSS・JavaScript・WordPress案件を中心に年間20件以上の制作実績を持つ。「難しい技術をわかりやすく」をモットーに、初心者〜中級者向けの技術記事を執筆。副業・フリーランス独立を目指す方に向けた情報発信に注力している。
公式サイト より
今すぐ
無料カウンセリング
を予約!