JavaScriptの非同期通信入門 ─ Promise / fetch / async/awaitを使いこなす
はじめに
JavaScript を使ってサーバーとデータのやり取りをするには「非同期通信」が欠かせません。
非同期通信は、Webページをリロードせずにデータを取得・送信できるため、スムーズでインタラクティブなユーザー体験を実現します。
本記事では、非同期通信の基本的な考え方や仕組み、主要な技術(Promise、fetch、async/await)をできるだけ分かりやすく解説します。
非同期処理に苦手意識がある方や、fetch や async/await をこれから本格的に使いたい方に向けた入門ガイドです。
1. 非同期通信
非同期通信の定義
非同期通信とは、「リクエストを送っても、処理の完了を待たずに他の処理を進めることができる通信方法」です。
非同期通信により、処理は逐次的に書かれていても、背後で別スレッドやイベントループにより「待機中」の処理をブロックせずに進行できます。
例えば、Webページ上でボタンを押下して、サーバーからデータを取得し、画面を更新する処理があった場合でも、データの取得が終わるまで JavaScript の他の処理が止まることはありません。
これによって、スムーズで快適な操作感を提供することができます。
同期通信との違い
同期通信は、コードを順番に処理し、1つの処理が終わるまで次の処理は行いません。
同期処理の場合、実行中の処理は1つだけになるため、直感的な動作になります。
しかし、同期的にブロックする処理が行われていた場合に問題が出ます。
同期処理ではその処理が終わるまで、次の処理へ進むことができないため他の操作が行えません。
処理方式 | 説明 | 特徴 |
---|---|---|
同期通信 | リクエストを送ると、完了するまで次の処理に進まない。 | 処理が順番通りに進行するため分かりやすい。待ち時間が長いと UI がフリーズする。 |
非同期通信 | リクエストを送っても、完了を待たずに次の処理へ進む。 | 操作性がよく、Webページのパフォーマンスが向上する。 |
非同期通信の必要性
JavaScript はシングルスレッドで動作します。
つまり、1つの処理しか同時に実行できません。
時間がかかる通信処理を同期的に行うと、他の操作(クリック、スクロール、入力など)が全てブロックされてしまいます。
非同期通信を使うことで、重たい処理を裏で実行しつつ、ユーザーの操作を阻害せずに動作させることが可能になります。
非同期通信の使用例
- ユーザー情報を取得してプロフィールを表示
- 商品の在庫状況を確認するAPI呼び出し
- 画像や動画の読み込み
- 入力補完(オートコンプリート)の候補取得
- ページネーションでの追加データの読み込み
よく使われる非同期通信手段(JavaScript)
- XMLHttpRequest(旧来の方法)
- fetch API(Promiseベースで現在主流)
- axios(ライブラリベース)
- WebSocket(双方向通信)
- Server-Sent Events(一方向のプッシュ通信)
本記事ではこの中でも「fetch API」を中心に、非同期処理の仕組みや書き方を掘り下げていきます。
2. Promise
Promise の定義
Promise とは、非同期処理の結果を「将来」受け取ることを約束するオブジェクトです。
簡単に言えば、「今は処理が終わってないけど、終わった後に結果(成功 or 失敗)を渡すよ」という受け取り予約券のようなものです。
Promise インスタンスと状態
Promise は new
演算子で Promise
のインスタンスを作成して利用します。
このときのコンストラクタには resolve
と reject
の引数を持つ executor
という関数を渡します。
executor
関数の中で非同期処理を行い、非同期処理が成功した場合は resolve
関数を呼び、失敗した場合は reject
関数を呼びます。
Promise には3種類の状態が存在します。
状態 | 意味 |
---|---|
fulfilled |
処理が resolve (成功)した状態 |
rejected |
処理が reject (失敗)した状態 |
pending |
処理が完了していない状態( fulfilled でも rejected でもない) |
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('成功しました');
} else {
reject('失敗しました');
}
});
Promise の基本的な使い方『.then() と .catch()』
.then()
は成功時(fulfilled)に実行され、.catch()
は失敗時(rejected)に実行されます。
.finally()
は成功・失敗を問わず、必ず最後に実行されます。
promise
.then((result) => {
console.log(result); // "成功しました"
})
.catch((error) => {
console.error(error); // "失敗しました"
});
Promise のメリット
以前の JavaScript では、非同期処理はコールバック関数で行っていました。
doSomething(function(result) {
doAnotherThing(result, function(response) {
yetAnotherThing(response, function(final) {
// ...
});
});
});
上記のコードのようにネストが深くなりやすく、可読性や保守性に難がありました。
Promise を使うことでコールバック地獄を回避でき、コードの見通しがよくなります。
非同期処理をフラットかつ直列的に記述できるようになり、エラー処理も一元化して管理が可能です。
Promise を返す関数の例
function wait(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`Waited ${ms}ms`);
}, ms);
});
}
wait(1000).then((msg) => console.log(msg)); // "Waited 1000ms"
このように、fetch を含め、多くのモダンAPIは Promise を返す設計になっています。
3. fetch
fetchとは
fetch とは、JavaScript で非同期にHTTP通信を行うための組み込み関数です。
Promise ベースで設計されており、.then()
や async/await
で扱うことができます。
fetch はサーバーからのデータの取得や送信などに使われます。
- REST APIからデータを取得する(GET)
- フォームのデータを送信する(POST)
- リソースを更新する(PUT、PATCH)
- データを削除する(DELETE)
fetch の基本的な書き方
fetch()
は Promise を返します。
response.json()
も Promise を返し、.then()
で結果を繋いでいきます。
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => {
console.log(data); // 取得したデータを使う
})
.catch((error) => {
console.error('エラーが発生しました:', error);
});
fetch の仕組み
-
fetch()
がHTTPリクエストを送信する。 - サーバーからレスポンスが返ってくる。
- レスポンスの中身は
Body
として分離されており、response.json()
などで取り出す。 - 最終的に、取得したデータを使って処理を行う。
POST リクエストの例(データ送信)
method
でリクエストの種類を指定し、headers
で送信形式を明示します。
body
に送信データを JSON 形式で記述します。
fetch('https://api.example.com/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Taro', age: 25 })
})
.then((res) => res.json())
.then((data) => {
console.log('成功:', data);
})
.catch((error) => {
console.error('エラー:', error);
});
fetch の注意点
- エラーが
catch
されない。
→ fetch は HTTP エラー(4xx, 5xx)を「通信成功(ステータス付き)」とみなすため、.catch()
では捕捉されず、res.ok
を使って判定する必要がある。 - レスポンスの形式に注意。
→res.json()
やres.text()
など、必要な形式に明示的に変換する。 - CORS制約
→ クロスドメイン通信には CORS(クロスオリジン)の対応が必要。
fetch('https://example.com/api')
.then(res => {
if (!res.ok) {
throw new Error(`HTTPエラー: ${res.status}`);
}
return res.json();
})
.then(data => {
console.log(data);
})
.catch(err => {
console.error(err);
});
4. async/await
async/await とは
async/await
は Promise のシンタックスシュガー(糖衣構文)で、Promise をよりシンプルかつ直感的に扱うための構文です。
async
関数は必ず Promise を返します。
関数内で await
を使うことで、非同期処理を「同期的に見える形」で記述することができ、try-catch
によってエラー処理が一元化できるため、可読性と保守性が大きく向上します。
-
async
:関数の先頭につけることで「非同期関数」として定義する。戻り値は Promise。 -
await
:Promise の完了を待つ命令。Promise が「解決(resolve
)」または「失敗(reject
)」されるまで、その後のコードは実行されない。
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('エラー:', error);
}
}
5.then
.then() とは
.then()
は Promise が解決(または拒否)された後、後続する処理を定義するためのメソッドです。
複数の .then()
を繋げることで、非同期処理を直列的に記述することができます。
-
.then()
は直前の Promise の結果を受け取る。 -
.then()
の戻り値は自動的に Promise に変換され、次の.then()
に渡される。 - 各
.then()
は前の処理が完了するまで待機する。(直列実行) -
.catch()
はチェーンの中でエラーが発生した場合に実行される。 -
.catch()
は.then()
と同じように Promise を返すため、続きの処理も再開可能。
fetch('https://api.example.com/data')
.then((response) => response.json()) // ここでPromiseが返る(レスポンスをJSONに変換)
.then((data) => {
console.log(data); // json変換後の結果が渡る(JSONデータの利用)
})
.catch((error) => {
console.error('通信エラー:', error);
});
new Promise((resolve) => {
resolve(1);
})
.then((val) => {
console.log(val); // 1
return val + 1;
})
.then((val) => {
console.log(val); // 2
return val + 1;
})
.then((val) => {
console.log(val); // 3
});
6. async/await と then の違いと使い分け
async/await
と .then()
はどちらも Promise を扱うための手段です。
どちらを使っても非同期処理の流れを構築できます。
async/await
を使うと、非同期処理が同期的に記述できるため、コールバック地獄を避けることができ、コードが直感的で読みやすくなります。一方で、並列処理の場合はやや工夫が必要です。
then
チェーンは柔軟に対応できる反面、ネストや中間値の扱いが煩雑になりがちです。
async/await と then の比較
項目 | .then() |
async/await |
---|---|---|
構文スタイル | 関数チェーン型 | 手続き型(同期風) |
可読性 | 複雑になるとネストが深くなりがち | 直感的で読みやすい |
エラー処理 |
.catch() を使用 |
try-catch で一括処理 |
中間値の保持 | チェーン内で工夫が必要 | 変数に代入可能、簡単 |
条件分岐やループ | やや書きにくい | 自然に記述可能 |
逐次処理(直列) | 明示的にチェーンをつなぐ必要がある |
await を順に書くだけでOK |
並列処理 |
.then() を分岐して書きやすい |
Promise.all() との併用が必要 |
コード比較Ⅰ:API 取得(直列)
.then() の場合
fetch('/user')
.then(res => res.json())
.then(user => {
return fetch(`/posts?userId=${user.id}`);
})
.then(res => res.json())
.then(posts => {
console.log(posts);
})
.catch(err => console.error(err));
async/await の場合
async function getUserPosts() {
try {
const userRes = await fetch('/user');
const user = await userRes.json();
const postsRes = await fetch(`/posts?userId=${user.id}`);
const posts = await postsRes.json();
console.log(posts);
} catch (err) {
console.error(err);
}
}
コード比較Ⅱ:並列実行
.then() の場合
Promise.all([
fetch('/user').then(res => res.json()),
fetch('/settings').then(res => res.json())
]).then(([user, settings]) => {
console.log(user, settings);
});
async/await の場合
async function fetchAll() {
try {
// 並列にリクエストを送信
const [userRes, settingsRes] = await Promise.all([
fetch('/user'),
fetch('/settings')
]);
// 並列にJSON変換を実行
const [user, settings] = await Promise.all([
userRes.json(),
settingsRes.json()
]);
console.log(user, settings);
} catch (err) {
console.error('通信エラー:', err);
}
}
レスポンスの .json()
も非同期処理であるため、1つずつ await
するよりも Promise.all()
で並列に変換処理を行うことで、待ち時間を短縮できます。
処理が互いに依存していない場合は、このように並列化を意識することでパフォーマンスが向上します。
async/await は for 文と相性が良い
await
は for
ループの中でも使えます。
then
では書きにくい逐次処理も自然に書くことができます。
async function processList(items) {
for (const item of items) {
const result = await fetchItem(item);
console.log(result);
}
}
なお、ループ内で await
を使うと、各処理が逐次実行されます。
全ての処理を同時に実行したい場合は、Promise.all()
と .map()
を併用すると並列処理になります。
async function processListInParallel(items) {
const results = await Promise.all(items.map(item => fetchItem(item)));
console.log(results);
}
7. 推奨方法(まとめ)
ここまで、非同期通信における Promise
、.then()
、async/await
の仕組みと使い分けについて解説してきました。
このセクションでは、実務や学習で「どれを使えばよいか」迷ったときの指針を簡潔に整理します。
基本的な推奨スタイル
基本は async/await
を優先的に使用するのが現代の主流です。
可読性と保守性が高いため、エラーの追跡やデバッグがしやすく、エラー処理を try-catch
で一括管理することができます。
- コードの可読性を重視したい場合。
- 複数の非同期処理を同期的に書きたい場合。(例:複数の API を順番に呼び出す)
- エラーハンドリングをシンプルにしたい場合。
async function fetchData() {
try {
const res = await fetch('/api/data');
const data = await res.json();
console.log(data);
} catch (err) {
console.error('取得失敗:', err);
}
}
.then() が有効なケース
- 非常にシンプルな非同期処理で、順番に処理を繋げる場合。
- 並列処理や条件によって処理フローが動的に変わる場合。
- コードやライブラリに
.then()
使用されており、一貫性を保ちたい場合。 - コールバック関数の中に非同期処理を挟みたい場合。(イベントハンドラなど)
Promise.all([fetchA(), fetchB()])
.then(([a, b]) => doSomething(a, b))
.catch(handleError);
7. AJAXとは
AJAX の定義
AJAX とは、Asynchronous JavaScript and XML の略で、「Webページを再読み込みせずに、非同期でサーバーと通信し、データを取得・送信する技術全般」を指します。
AJAX は特定の技術やAPI名ではなく、JavaScript、XMLHttpRequest(またはfetch)、DOM操作など複数の技術を組み合わせた非同期通信の総称です。
現在では、XML の代わりに JSON を使うのが主流ですが、"AJAX" という用語は引き続き使われています。
AJAX の仕組み
AJAX による非同期通信の流れは以下の通りです。
- JavaScript から XMLHttpRequestで HTTP リクエストを送信。
- サーバーが XML や JSON 形式のレスポンスを返す。
- JavaScript がレスポンスを受け取り、DOM を動的に更新。
- ページ全体をリロードせずに画面が更新される。
AJAX の使用例(XMLHttpRequest)
以下は、XMLHttpRequest を用いたシンプルな AJAX 処理の例です。
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
}
};
xhr.send();
AJAX の現在
AJAX という言葉には古い印象もありますが、「ページをリロードせずにサーバーと通信してUIを動的に更新する」という考え方自体は、現在のWebアプリケーションの中核をなしています。
XMLHttpRequest に代わり、現在では Promise ベースでシンプルに扱える fetch API が主流です。
現代でも、「AJAX通信」「AJAXリクエスト」といった言葉は、fetch や axios を使った非同期通信の文脈で広く使われています。
8.fetch と AJAX の関係
fetch と AJAX の比較
fetch は XMLHttpRequest の代替として新たに登場した JavaScript の API です。
項目 |
XMLHttpRequest (AJAX) |
fetch (モダンAPI) |
---|---|---|
非同期通信の手法 | XMLHttpRequest | Promiseベース |
構文 | 複雑・冗長 | 簡潔 |
レスポンス処理 |
responseText などを明示的に処理 |
res.json() など柔軟な変換が可能 |
エラーハンドリング | ステータスコードや readyState の手動制御が必要 |
.catch() または try-catch で一元管理 |
同期/非同期 | 両対応(ただし同期は非推奨) | 非同期のみ |
拡張性 | カスタマイズがやや面倒 | リクエストの構成が柔軟で使いやすい |
ブラウザ対応 | ほとんどのブラウザでサポート | モダンブラウザでサポート、古いブラウザはポリフィルで対応可 |
実装例比較
XMLHttpRequest と fetch を比較すると、fetch の方がコード量が少なく、構造も明確で扱いやすいことが分かります。
XMLHttpRequest(従来のAJAX)の場合
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onload = () => {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
}
};
xhr.onerror = () => {
console.error('通信エラー');
};
xhr.send();
fetch(モダンな非同期通信)の場合
fetch('/api/data')
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => console.error('通信エラー:', err));
一部のレガシーシステムやIE対応が必須な現場では、XMLHttpRequest が使われることもあります。
ただし、新規開発では基本的に fetch を使うのがベストプラクティスです。
まとめ
非同期通信は、現代のWebアプリケーションにおいて非常に重要な要素です。
JavaScript の fetch
や Promise
、async/await
を活用することで、より直感的かつ効率的なコードが書けるようになります。
また、処理の流れや依存関係を意識して、逐次実行と並列実行を適切に使い分けることが、パフォーマンス向上の鍵となります。
ぜひ今回の内容をもとに、実際の開発に役立ててみてください。
Discussion