JavaScript:fetchとJSON
1学習の目的
- fetch でWeb APIからデータを取得し、JSONとして扱えるようになる。自分のページに外部のデータを取り込めるようになる。
- 通信の失敗を正しく検知して処理できるようになる。fetch特有の「404でもエラーにならない」という落とし穴を回避できるようになる。
2基礎解説
| やりたいこと | 書き方 | 備考 |
|---|---|---|
| データを取得 | const res = await fetch(url); | Promise を返す |
| 成功を確認 | if (!res.ok) { … } | 必須。404は例外にならない |
| JSONに変換 | const data = await res.json(); | これにも await |
| データを送る | fetch(url, { method: "POST", … }) | 第2引数でオプション指定 |
- 404 や 500 でも fetch は失敗しない。「サーバーから返事が来た」時点で成功扱いになる。res.ok か res.status を自分で確認する必要がある。
- await が2回必要。1回目は通信の完了を待ち、2回目は本文の読み取りを待つ。res.json() にも await を付け忘れないこと。
- catch に飛ぶのはネットワークに繋がらないときだけ(オフライン、URLの誤り、ドメインが存在しない)。エラーの種類は TypeError。
- データを送るときは method・headers・body を指定する。body は JSON.stringify() で文字列にしてから渡す。
- 壊れたJSONが返ると res.json() が SyntaxError を投げる。サーバーがHTMLのエラーページを返すことは実際にあるので、try で囲む。
現場使用例:ユーザー情報の取得、商品一覧の読み込み、フォーム内容の送信、天気や為替の表示。現代のWebアプリは、ほぼすべてこの仕組みで動いている。
async function getUser(id) {
try {
const res = await fetch(`https://api.example.com/users/${id}`);
// ここが最重要:404でも fetch 自体は成功する
if (!res.ok) {
throw new Error(`取得失敗(${res.status})`);
}
const data = await res.json(); // ← ここにも await
return data;
} catch (e) {
console.log(`エラー:${e.message}`);
return null;
}
}
// データを送る場合
await fetch("https://api.example.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "田中" }),
});
3基本ドリル(10問)
出力 == 田中
await fetch(...) で応答を取り、await res.json() でデータに変換する。await が2回必要。
async function main() {
const res = await fetch(`${API}/users/1`);
const data = await res.json();
console.log(data.name);
}
main();
await が2回必要なのがポイント。1回目は通信の完了、2回目は本文の読み取りを待っている。
A. catch に飛ぶ B. 成功扱いになり res.ok が false になる C. null が返る D. 自動で再試行される 選択★☆☆無料
解答 == B
「サーバーから返事が来た」ことをどう解釈するか。
B
fetch が失敗と見なすのはネットワークに繋がらないときだけ。404も500も「返事が来た」ので成功扱いになる。だから res.ok の確認が必須。
出力 == 200 / true
res.status と res.ok をそのまま使う。json() は不要。
async function main() {
const res = await fetch(`${API}/users/1`);
console.log(`${res.status} / ${res.ok}`);
}
main();
res.ok はステータスが200番台なら true。細かい番号を見る前に、まずこれで成否を判定するのが簡潔。
async function main() {
const res = await fetch(`${API}/users/1`);
const data = await res.____();
console.log(data.name);
}
main();
出力 == 田中
JSON形式で読み取るメソッド。
json
文字列として読みたいときは res.text()。どちらも await が必要で、本文の読み取り自体が非同期処理になっている。
A. 404が返ったとき B. 500が返ったとき C. ネットワークに繋がらないとき D. JSONが空のとき 選択★☆☆無料
解答 == C
サーバーまで届いたかどうかが分かれ目。
C
オフライン、URLのつづりミス、ドメインが存在しない——そもそも通信が成立しない場合だけ。エラーの種類は TypeError になる。
出力 == 取得失敗(404)
if (!res.ok) で判定し、res.status を埋め込む。
async function main() {
const res = await fetch(`${API}/nothing`);
if (!res.ok) {
console.log(`取得失敗(${res.status})`);
}
}
main();
この確認を書かないと、404のHTMLをJSONとして読もうとしてエラーになる。原因が分かりにくいバグになるので、必ず最初に書く。
async function main() {
const res = await fetch(`${API}/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.____({ name: "新規ユーザー" }),
});
console.log(res.status);
}
main();
出力 == 200
オブジェクトをJSON文字列にするメソッド。末尾に fy が付く。
stringify
body には文字列しか渡せない。オブジェクトをそのまま渡すと [object Object] という文字列が送られてしまう。
A. データ本体 B. Promise C. 文字列 D. undefined 選択★★☆無料
解答 == B
json() も非同期のメソッド。
B
JS15で学んだとおりawait を忘れると Promise が入る。data.name が undefined になったら、まず await の付け忘れを疑う。
出力2行。1. 田中 / 2. 鈴木
返ってくるのは配列なので、そのまま forEach や for...of で回せる。
async function main() {
const res = await fetch(`${API}/users`);
const users = await res.json();
users.forEach((u, i) => {
console.log(`${i + 1}. ${u.name}`);
});
}
main();
JSONの配列は、変換した時点でただのJavaScriptの配列になる。JS09の map や filter がそのまま使える。
出力 == 通信できません
try / catch で囲む。ネットワークエラーは catch に飛ぶ。
async function main() {
try {
await fetch("http://127.0.0.1:9999/x");
} catch (e) {
console.log("通信できません");
}
}
main();
catch に来るのはこのケースだけ。404などのHTTPエラーとは扱いが違うので、両方を書いて初めて通信処理が完成する。
4実践シナリオ(5問)
出力2行が完全一致
!res.ok なら throw し、catch で null を返す。呼び出し側で null を判定する。
async function fetchJson(url) {
try {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return await res.json();
} catch (e) {
return null;
}
}
async function main() {
const user = await fetchJson(`${API}/users/1`);
console.log(user === null ? "取得できませんでした" : user.name);
const missing = await fetchJson(`${API}/nothing`);
console.log(missing === null ? "取得できませんでした" : missing.name);
}
main();
成功か null かに単純化して返す設計。呼び出す側は通信の細かい事情を知らなくてよくなり、if 1つで分岐できる。実務でよく使われる形。
URLは API + "/users/1"、API + "/nothing"、"http://127.0.0.1:9999/x"(ラベルは順に users/1、nothing、bad-host)。 コーディング★★☆無料
出力3行が完全一致
try の中で res.ok を判定し、HTTPエラーは自分で出力する。catch に来たものだけが通信エラー。
async function check(label, url) {
try {
const res = await fetch(url);
if (!res.ok) {
console.log(`[${label}] HTTPエラー:${res.status}`);
return;
}
console.log(`[${label}] 成功`);
} catch (e) {
console.log(`[${label}] 通信エラー`);
}
}
async function main() {
await check("users/1", `${API}/users/1`);
await check("nothing", `${API}/nothing`);
await check("bad-host", "http://127.0.0.1:9999/x");
}
main();
HTTPエラーと通信エラーは別物。前者はサーバーに届いたうえでの応答、後者はそもそも届いていない。監視やログでは、この2つを区別して記録する。
出力2行が完全一致
res.json() が SyntaxError を投げるので、try で囲めば捕まえられる。
async function load(url) {
try {
const res = await fetch(url);
const data = await res.json();
console.log(data.name);
} catch (e) {
console.log("JSONの解析に失敗しました");
}
}
async function main() {
await load(`${API}/broken`);
await load(`${API}/users/1`);
}
main();
サーバーがHTMLのエラーページを返すことは実際にある。ステータスが200でも中身が壊れている可能性があるので、json() も try の中に入れておく。
出力2行が完全一致
Promise.all の中で fetch と json() をまとめて行う関数を渡す。分割代入で受け取る。
const fetchJson = async (url) => {
const res = await fetch(url);
return res.json();
};
async function main() {
const [user, list] = await Promise.all([
fetchJson(`${API}/users/1`),
fetchJson(`${API}/users`),
]);
console.log(`ユーザー:${user.name}`);
console.log(`一覧:${list.length}件`);
}
main();
互いに依存しない通信は並列にする(JS15)。順に await すると待ち時間が足し算になるが、Promise.all なら遅いほうの時間で済む。
出力 == 送信成功(200)
fetch の第2引数に { method, headers, body } を渡す。body は JSON.stringify で文字列にする。
async function main() {
const res = await fetch(`${API}/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "新規ユーザー",
email: "new@example.com",
}),
});
if (res.ok) {
console.log(`送信成功(${res.status})`);
} else {
console.log(`送信失敗(${res.status})`);
}
}
main();
3点セット(method・headers・body)がPOSTの基本形。Content-Type を書かないとサーバーがJSONだと認識せず、データを受け取れないことがある。
5仕上げ課題
利用できるエンドポイント(API は基準URL):
・/users … ユーザー配列 [{ id, name }, …](2件)
・/users/1 … { id, name, email }
・/nothing … 404 が返る
・/broken … 壊れたJSONが返る
仕様:
① 安全に取得する関数 safeFetch(url) を作る。成功なら { ok: true, data }、失敗なら { ok: false, reason } を返す
reason は、HTTPエラーなら "HTTP 404" の形式、JSON解析失敗なら "不正なJSON"、通信不可なら "接続できません"
② /users と /users/1 を並列で取得し、成功したら一覧と詳細を表示
③ /nothing と /broken も順に試し、失敗理由を表示
④ 最後に成功件数と失敗件数を出力
出力仕様(8行):
=== ユーザー情報 ===
[一覧] 2件(田中、鈴木)
[詳細] 田中 <tanaka@example.com>
---
[nothing] 失敗:HTTP 404
[broken] 失敗:不正なJSON
---
成功2件 / 失敗2件
出力8行が完全一致
safeFetch では res.ok を確認してから res.json() を try で囲む。JSON解析の失敗と通信の失敗を区別するには、fetch と json() を別々の try で囲むか、エラーの種類(e.name === "SyntaxError")で判定する。名前の連結は map(u => u.name).join("、")。
async function safeFetch(url) {
let res;
try {
res = await fetch(url);
} catch (e) {
return { ok: false, reason: "接続できません" };
}
if (!res.ok) {
return { ok: false, reason: `HTTP ${res.status}` };
}
try {
const data = await res.json();
return { ok: true, data };
} catch (e) {
return { ok: false, reason: "不正なJSON" };
}
}
async function main() {
console.log("=== ユーザー情報 ===");
let ok = 0;
let ng = 0;
const [list, detail] = await Promise.all([
safeFetch(`${API}/users`),
safeFetch(`${API}/users/1`),
]);
if (list.ok) {
const names = list.data.map((u) => u.name).join("、");
console.log(`[一覧] ${list.data.length}件(${names})`);
ok++;
}
if (detail.ok) {
console.log(`[詳細] ${detail.data.name} <${detail.data.email}>`);
ok++;
}
console.log("---");
for (const [label, url] of [["nothing", `${API}/nothing`], ["broken", `${API}/broken`]]) {
const result = await safeFetch(url);
if (!result.ok) {
console.log(`[${label}] 失敗:${result.reason}`);
ng++;
}
}
console.log("---");
console.log(`成功${ok}件 / 失敗${ng}件`);
}
main();
通信で起きうる3種類の失敗を、すべて別々に扱えているのがこの課題の到達点。
① 接続できない(catch)② サーバーは応答したがエラー(res.ok が false)③ 応答したが中身が壊れている(json() が SyntaxError)——この3つを区別せずに catch ひとつでまとめてしまうと、「なぜ失敗したのか」が分からなくなる。ユーザーに表示すべきメッセージも、原因によって変わるはずだ。
そして safeFetch が { ok, data, reason } という決まった形で返している点も重要だ。呼び出す側は if (result.ok) と書くだけでよく、fetch の細かい事情を知らなくて済む。複雑さを関数の中に閉じ込める——JS08から一貫して学んできた設計の考え方が、ここでも効いている。
次章では、取得したデータをブラウザに保存する方法を学ぶ。JS12で作ったToDoアプリの「再読み込みすると消える」という弱点が、ついに解決する。