📖

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 のインスタンスを作成して利用します。
このときのコンストラクタには resolvereject の引数を持つ 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 の仕組み

  1. fetch() がHTTPリクエストを送信する。
  2. サーバーからレスポンスが返ってくる。
  3. レスポンスの中身は Body として分離されており、response.json() などで取り出す。
  4. 最終的に、取得したデータを使って処理を行う。

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 文と相性が良い

awaitfor ループの中でも使えます。
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 による非同期通信の流れは以下の通りです。

  1. JavaScript から XMLHttpRequestで HTTP リクエストを送信。
  2. サーバーが XML や JSON 形式のレスポンスを返す。
  3. JavaScript がレスポンスを受け取り、DOM を動的に更新。
  4. ページ全体をリロードせずに画面が更新される。

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 の fetchPromiseasync/await を活用することで、より直感的かつ効率的なコードが書けるようになります。

また、処理の流れや依存関係を意識して、逐次実行と並列実行を適切に使い分けることが、パフォーマンス向上の鍵となります。

ぜひ今回の内容をもとに、実際の開発に役立ててみてください。

Discussion