非同期処理とFetch
Promise, async/await, fetch API で外部データを取得する方法を、PHP との比較で学びます。
このチャプターで学ぶこと
- 同期処理と非同期処理の違いを理解できる
- Promise の基本概念を理解できる
- async/await で非同期処理を書ける
- fetch API で JSON データを取得・表示できる
- エラーハンドリング(try/catch)ができる
非同期処理とFetch
このチャプターでは、JavaScriptにおける最も重要かつ初学者がつまづきやすい概念のひとつ、非同期処理を学びます。外部APIからデータを取得してページに表示するという、現代のWebアプリの根幹となるパターンを習得しましょう。
なぜ非同期が必要か
まず「同期処理」と「非同期処理」の違いを理解するところから始めます。
PHPの場合(同期・ブロッキング)
PHPはリクエスト単位で処理を実行します。一行ずつ上から下へ順番に実行され、ある処理が終わるまで次の処理は始まりません(ブロッキング)。
<?php
// PHP: 上から順に実行される(ブロッキング)
$data = file_get_contents('https://api.example.com/users'); // ここで待つ
// ↑ この処理が完了するまで次の行は実行されない
$users = json_decode($data, true); // データ取得後に実行される
echo "ユーザー数: " . count($users);
PHPではこれで問題ありません。なぜならPHPはリクエストごとに独立したプロセスで動き、1ユーザーが待っても他のユーザーへの影響が少ないからです。
JavaScriptの場合(シングルスレッド・ノンブロッキング)
JavaScriptはブラウザ上で動くシングルスレッドの言語です。1つのスレッドしかないため、何かをブロッキングで待ち続けるとページ全体がフリーズしてしまいます。
PHP のイメージ:
リクエストA → [API待機中...] → 処理完了
リクエストB → [API待機中...] → 処理完了 ← 並行して動ける
JavaScript のイメージ:
UIスレッド → [API待機中(ブロック)...] → ページが完全にフリーズ!
↑ この間、ボタンクリックも入力も何も受け付けない
だから JavaScriptでは、時間のかかる処理(通信・ファイル読み込みなど)を「バックグラウンドで待って、終わったら教えて」という形で扱います。これが非同期処理です。
非同期のイメージ:
UIスレッド → APIリクエスト開始 → [続けて他の処理...ボタンも動く]
↓
[バックグラウンドで通信中]
↓
通信完了!コールバックを実行
コールバック(昔のやり方)
非同期処理の最初のパターンはコールバック関数でした。「終わったらこの関数を呼んでね」と渡す方式です。
// setTimeout はコールバックの典型例
// 「2秒後にこの関数を呼ぶ」という指示
setTimeout(() => {
console.log('2秒経ちました');
}, 2000);
console.log('これはすぐ表示される'); // こちらが先に実行される
しかし非同期処理が複数ネストすると、いわゆる「コールバック地獄」に陥ります。
// コールバック地獄の例(読みにくい!)
fetchUser(userId, (user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
fetchAuthor(comments[0].authorId, (author) => {
// ここまでネストするともう限界...
console.log(author.name);
});
});
});
});
コールバック地獄は現代のJavaScript開発では避けられます。今は Promise と async/await を使うことで、入れ子にならないすっきりとしたコードが書けます。コールバックパターン自体は addEventListener のように今でも使われていますが、非同期の連鎖には使いません。
Promiseの基本
Promise は「非同期処理の結果を表すオブジェクト」です。処理が完了したとき(成功または失敗)に通知を受け取れます。
Promiseの3つの状態
pending(待機中) → fulfilled(成功)
↘ rejected(失敗)
- pending: 非同期処理が実行中
- fulfilled: 処理が成功した
- rejected: 処理が失敗した(エラーが発生した)
Promiseの作成と使用
// Promiseを作る(ライブラリが中でやってくれることが多い)
const promise = new Promise((resolve, reject) => {
// 非同期な処理を実行...
const success = true; // 仮に成功したとして
if (success) {
resolve('成功しました!'); // fulfilled 状態にする
} else {
reject(new Error('失敗しました')); // rejected 状態にする
}
});
// Promiseを使う(.then / .catch でつなぐ)
promise
.then((result) => {
// fulfilled のとき実行される
console.log(result); // "成功しました!"
})
.catch((error) => {
// rejected のとき実行される
console.error(error.message);
})
.finally(() => {
// 成功・失敗どちらでも最後に実行される
console.log('処理が完了しました');
});
Promiseのチェーン
.then() は新しいPromiseを返すため、連続した非同期処理をフラットに書けるのが大きなメリットです。
// コールバック地獄を Promise で書き直した例
fetchUser(userId)
.then((user) => fetchPosts(user.id)) // user が返ってきたら次へ
.then((posts) => fetchComments(posts[0].id)) // posts が返ってきたら次へ
.then((comments) => fetchAuthor(comments[0].authorId))
.then((author) => {
console.log(author.name);
})
.catch((error) => {
// どこかでエラーが起きたら全部ここでキャッチできる
console.error(error);
});
入れ子がなくなり、ずっと読みやすくなりました。
async / await
async/await は Promise の上に作られた構文です。非同期処理を同期処理のように書けるようにする糖衣構文(シンタックスシュガー)で、現代のJavaScriptではこちらが主流です。
基本構文
// async キーワードを関数につける → その関数は必ずPromiseを返す
async function fetchData() {
// await を使うと Promise の解決を待てる(ブロッキングではなく、他の処理は動く)
const result = await somePromise;
return result;
}
.then() と async/await の比較
全く同じ処理を2つの書き方で比べてみましょう。
// 書き方1: .then() チェーン
function getUserName(userId) {
return fetchUser(userId)
.then((user) => {
return user.name;
})
.catch((error) => {
console.error('取得に失敗しました:', error);
});
}
// 書き方2: async/await(こちらが推奨)
async function getUserName(userId) {
try {
const user = await fetchUser(userId);
return user.name;
} catch (error) {
console.error('取得に失敗しました:', error);
}
}
async/await を使うと、まるでPHPの同期処理のように上から下へ読めるコードになります。
.then() と async/await は同じことを表現しています。どちらを使っても動きますが、コードの読みやすさの観点から async/await を基本として使うことをおすすめします。既存コードを読むときに .then() が登場することもあるため、両方の書き方を理解しておきましょう。
await は async 関数の中でしか使えない
// トップレベルのawait(モダンなブラウザ・Node.jsのESモジュールでは使える)
const data = await fetchData(); // OK(最新環境)
// 古い環境では即時実行関数(IIFE)で囲む
(async () => {
const data = await fetchData();
console.log(data);
})();
fetch API の基本
fetch はブラウザ組み込みのHTTPリクエスト関数です。URLを渡すと Promise を返します。
GETリクエスト
// fetch の基本的な使い方
async function getUsers() {
// 1. fetch でリクエストを送る(Promiseが返る)
const response = await fetch('https://jsonplaceholder.typicode.com/users');
// 2. レスポンスのJSONをパース(これもPromiseが返る)
const users = await response.json();
console.log(users); // ユーザーの配列
}
getUsers();
fetch の処理は2段階になっています:
fetch(url)→ HTTP通信が完了して レスポンスオブジェクト を返すresponse.json()→ ボディをJSONとして読み込んで JavaScriptのオブジェクト に変換する
Laravelのコントローラーで例外が起きると自動的にエラーレスポンスになりますが、fetch では少し違います。
const response = await fetch('https://api.example.com/users/99999');
// 404 Not Found でも fetch はエラーを throw しない!
console.log(response.ok); // false(ステータス 200-299 のとき true)
console.log(response.status); // 404
// 必ず response.ok を確認して、失敗時は手動でthrowする
if (!response.ok) {
throw new Error(`HTTP エラー: ${response.status}`);
}ネットワーク自体の障害(インターネット切断など)は catch でキャッチできますが、404や500は response.ok で手動チェックが必要です。
レスポンスをDOMに表示する
fetch とDOM操作を組み合わせて、APIから取得したデータをページに表示してみましょう。
<!-- HTML -->
<div id="user-list"></div>
// JSONPlaceholder(テスト用の無料API)からユーザーを取得して表示する
async function displayUsers() {
const container = document.querySelector('#user-list');
// データを取得
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await response.json();
// 各ユーザーのカードを生成してDOMに追加
users.forEach((user) => {
const card = document.createElement('div');
card.classList.add('user-card');
// ユーザー名
const name = document.createElement('h3');
name.textContent = user.name;
// メールアドレス
const email = document.createElement('p');
email.textContent = user.email;
// 会社名
const company = document.createElement('p');
company.textContent = `会社: ${user.company.name}`;
card.appendChild(name);
card.appendChild(email);
card.appendChild(company);
container.appendChild(card);
});
}
// ページ読み込み後に実行
document.addEventListener('DOMContentLoaded', displayUsers);
前のチャプターで学んだDOM操作と、このチャプターで学んだ fetch を組み合わせるとこうなります。これがフロントエンドの基本パターンです。
エラーハンドリング
本番のアプリでは必ずエラーハンドリングを実装します。ネットワーク障害・サーバーエラー・予期しないデータ形式など、様々な問題が起こりえます。
try/catch の基本
async function fetchData(url) {
try {
const response = await fetch(url);
// HTTP エラーを手動でチェック
if (!response.ok) {
throw new Error(`サーバーエラー: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// ネットワークエラーや上の throw がここでキャッチされる
console.error('データの取得に失敗しました:', error.message);
throw error; // 呼び出し元にもエラーを伝える
}
}
ローディング状態とエラー表示を含む完全な実装
<!-- HTML -->
<div id="app">
<div id="loading" class="hidden">読み込み中...</div>
<div id="error" class="hidden"></div>
<div id="content"></div>
</div>
/* CSS */
.hidden { display: none; }
.error-message { color: red; padding: 1rem; border: 1px solid red; border-radius: 4px; }
// ユーティリティ関数
function showLoading() {
document.querySelector('#loading').classList.remove('hidden');
document.querySelector('#error').classList.add('hidden');
document.querySelector('#content').innerHTML = '';
}
function hideLoading() {
document.querySelector('#loading').classList.add('hidden');
}
function showError(message) {
const errorEl = document.querySelector('#error');
errorEl.classList.remove('hidden');
errorEl.textContent = message; // textContent を使って XSS を防ぐ
}
// メインの関数
async function loadUsers() {
showLoading(); // ローディング表示開始
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error(`データの取得に失敗しました(${response.status})`);
}
const users = await response.json();
const content = document.querySelector('#content');
users.forEach((user) => {
const div = document.createElement('div');
div.textContent = user.name;
content.appendChild(div);
});
} catch (error) {
// ユーザーにわかりやすいメッセージを表示
showError('データを読み込めませんでした。ネットワーク接続を確認してください。');
console.error(error); // 開発用に詳細はコンソールに出す
} finally {
hideLoading(); // 成功・失敗に関わらずローディングを消す
}
}
document.addEventListener('DOMContentLoaded', loadUsers);
try/catch/finally の使い方はPHPと同じ感覚で使えます。
<?php
// PHP の try/catch(同じ概念)
try {
$response = Http::get('https://api.example.com/users');
$users = $response->json();
} catch (\Exception $e) {
Log::error($e->getMessage());
return back()->withErrors(['message' => 'データの取得に失敗しました']);
} finally {
// 最後に必ず実行
}
POSTリクエスト
データをサーバーに送信するには、fetch にオプションを渡します。
async function createPost(title, body) {
const postData = {
title: title,
body: body,
userId: 1,
};
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST', // HTTPメソッド
headers: {
'Content-Type': 'application/json', // JSONを送る宣言
},
body: JSON.stringify(postData), // オブジェクトをJSON文字列に変換
});
if (!response.ok) {
throw new Error('投稿の作成に失敗しました');
}
const createdPost = await response.json();
console.log('作成されました:', createdPost);
return createdPost;
}
Laravelで言えば、フォームから POST する感覚に近いです。ただし Content-Type: application/json ヘッダーをつけて JSON.stringify でボディを作る点が異なります。
// PHP (Laravel): Httpファサードで POST
$response = Http::post('https://api.example.com/posts', [
'title' => $title,
'body' => $body,
]);
// JavaScript: fetch で POST
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, body }),
});
PUT / DELETE も同様
// PUT リクエスト(更新)
const response = await fetch(`https://api.example.com/posts/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedData),
});
// DELETE リクエスト(削除)
const response = await fetch(`https://api.example.com/posts/${id}`, {
method: 'DELETE',
});
JSONPlaceholder API から投稿の最初の10件を取得して、カード形式で表示してみましょう。
要件:
https://jsonplaceholder.typicode.com/postsから投稿を取得する- 最初の10件だけ表示する(
slice(0, 10)を使う) - 各投稿のタイトルと本文をカードで表示する
- データ取得中は「読み込み中…」を表示する
- エラーが起きたらエラーメッセージを表示する
HTML:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>投稿一覧</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 1rem; }
.card { border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; }
.card h3 { margin: 0 0 0.5rem; font-size: 1rem; text-transform: capitalize; }
.card p { margin: 0; color: #64748b; font-size: 0.875rem; }
.loading { text-align: center; color: #64748b; padding: 2rem; }
.error { color: #dc2626; border: 1px solid #dc2626; border-radius: 8px; padding: 1rem; }
</style>
</head>
<body>
<h1>投稿一覧</h1>
<div id="loading" class="loading">読み込み中...</div>
<div id="error" class="error" style="display:none;"></div>
<div id="posts"></div>
<script src="main.js"></script>
</body>
</html>JavaScript(main.js)の実装例:
async function loadPosts() {
const loadingEl = document.querySelector('#loading');
const errorEl = document.querySelector('#error');
const postsEl = document.querySelector('#posts');
try {
// APIからデータを取得
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error(`エラーが発生しました: ${response.status}`);
}
const allPosts = await response.json();
// 最初の10件だけ使う
const posts = allPosts.slice(0, 10);
// ローディングを非表示にする
loadingEl.style.display = 'none';
// 各投稿のカードを作成
posts.forEach((post) => {
const card = document.createElement('div');
card.classList.add('card');
const title = document.createElement('h3');
title.textContent = post.title; // XSS対策でtextContentを使う
const body = document.createElement('p');
body.textContent = post.body;
card.appendChild(title);
card.appendChild(body);
postsEl.appendChild(card);
});
} catch (error) {
// エラー表示
loadingEl.style.display = 'none';
errorEl.style.display = 'block';
errorEl.textContent = 'データを読み込めませんでした。しばらくしてから再度お試しください。';
console.error(error);
}
}
loadPosts();発展課題: ユーザーID別にフィルタリングするボタンを追加してみましょう。https://jsonplaceholder.typicode.com/posts?userId=1 のようにクエリパラメーターを使えます。
// URLSearchParams を使ってクエリパラメーターを構築する
const params = new URLSearchParams({ userId: 1, _limit: 10 });
const url = `https://jsonplaceholder.typicode.com/posts?${params}`;
const response = await fetch(url); まとめ
非同期処理とFetch APIのポイントをまとめます:
- なぜ非同期か: JavaScriptはシングルスレッド。同期的にブロックするとUIがフリーズするため、非同期で処理を待つ
- コールバック: 昔のパターン。ネストが深くなると「コールバック地獄」になる
- Promise: 非同期処理の結果を表すオブジェクト。
pending→fulfilled/rejectedの状態遷移 - async/await: Promiseを同期処理のように読めるシンタックスシュガー。基本的にこちらを使う
- fetch API: ブラウザ組み込みのHTTPクライアント。2段階で使う(
fetch()→.json()) - エラーハンドリング:
response.okで HTTPエラーをチェック、try/catchで例外をキャッチ、finallyでローディング状態をリセット - POST/PUT/DELETE:
fetchの第2引数でメソッド・ヘッダー・ボディを指定する
これでJavaScriptのDOM操作・イベント処理・非同期処理という3つの柱が揃いました。次のステップでは、これらの知識を活かして React を学びます。Reactはここまで学んだDOM操作や非同期処理を抽象化して、より宣言的にUIを構築できるライブラリです。