JavaScriptの非同期処理とは

2025-07-28

はじめに

「非同期処理」の概念はJavaScriptにおいて非常に重要であり、現代のWeb開発では避けて通れません。

この記事では、非同期処理とは何か、なぜ必要なのか、どのように動作するのかを、初心者の方にもわかりやすく詳しく解説していきます。

非同期処理とは何か?

基本的な定義

非同期処理(Asynchronous Processing)とは、コードの実行が上から順番に(同期的に)進むのではなく、特定の処理が完了するのを待たずに次の処理を進めることができるプログラミングの手法です。

対照的なのが同期処理(Synchronous Processing)で、これはコードが書かれた順番に1つずつ処理が実行され、前の処理が終わるまで次の処理は開始されません。

同期処理の特徴

  • タスクが順番に実行され、前のタスクが完了するまで次のタスクは開始されない
  • 実行順序が予測しやすい
  • UIの応答が停止する可能性がある(ブロッキング)
  • コードがシンプルで理解しやすい

非同期処理の特徴

  • タスクの開始後、完了を待たずに次のタスクを実行できる
  • 時間のかかる処理(API呼び出し、ファイル読み込みなど)で有用
  • UIの応答性を維持できる(ノンブロッキング)
  • コールバック、Promise、async/awaitなどの方法で実装

JavaScriptのイベントループ

JavaScriptはシングルスレッドですが、非同期処理を可能にする「イベントループ」メカニズムを持っています。

時間のかかる操作はバックグラウンドで処理され、完了するとコールバックキューに追加され、コールスタックが空になったときに実行されます。

日常生活での例え

非同期処理を理解するために、日常生活の例で考えてみましょう。

同期処理の例: レストランの注文

  1. 客が注文する
  2. 料理人が注文を受けて料理を作る
  3. 料理が完成するまで客は待つ
  4. 料理が完成したら客は食べ始める
  5. 次の客の注文を受ける

この方法では、1人の客の料理が完成するまで次の客の注文を受け付けられません。非常に非効率です。

非同期処理の例: 実際のレストラン

  1. 客Aが注文する
  2. 料理人は客Aの注文をキッチンに伝える
  3. すぐに客Bの注文を受け付ける
  4. 料理人が並行して料理を作る
  5. 料理が完成した客から順に提供する

これが非同期処理の考え方です。複数の作業を並行して進めることで、全体の効率を上げることができます。

なぜ非同期処理が必要なのか?

JavaScriptのシングルスレッドモデル

JavaScriptは「シングルスレッド」で動作します。
これは、一度に1つのことしか処理できないという意味です。

他の言語のようにマルチスレッドで並列処理を行うことができません。
しかし、Webブラウザでは多くのタスクを同時に処理する必要があります。

  • ユーザーのクリックやキー入力への反応
  • サーバーからのデータ取得
  • アニメーションの描画
  • タイマー処理

これらの処理を同期処理だけで行うと、時間のかかる処理(例えばサーバーからのデータ取得)の間、ブラウザは完全にフリーズしてしまいます。

ブラウザ環境での制約

Webブラウザでは、以下のような時間のかかる処理が頻繁に発生します。

  1. ネットワークリクエスト: API呼び出し、データ取得
  2. ファイル操作: ローカルファイルの読み書き
  3. データベース操作: IndexedDBへのアクセス
  4. タイマー処理: setTimeoutやsetInterval

これらの処理を同期的に行うと、ユーザー体験が著しく損なわれます。
非同期処理はこの問題を解決するための重要な手法なのです。

非同期処理の動作原理

イベントループとコールバックキュー

JavaScriptの非同期処理は「イベントループ」という仕組みで実現されています。

簡単に説明すると以下のようになります。

  1. すべての同期処理は「コールスタック」で実行される
  2. 非同期処理はWeb APIに渡され、バックグラウンドで実行される
  3. 非同期処理が完了すると、そのコールバック関数が「コールバックキュー」に追加される
  4. イベントループはコールスタックが空になると、コールバックキューから関数を取り出して実行する

この仕組みにより、長時間かかる処理でもメインの処理フローをブロックせずに実行できるのです。

非同期処理の具体例

実際のコードで見てみましょう。console.log("処理1")console.log("処理3")は同期的に順に実行されますが、setTimeout()内の「処理2 (非同期)」は1秒後に実行されます。

console.log("処理1");

setTimeout(() => {
  console.log("処理2 (非同期)");
}, 1000);

console.log("処理3");

このコードの実行結果は以下の通りです。

処理1
処理3
処理2 (非同期)

となります。したがって、出力順は「処理1 → 処理3 → 処理2」となり、イベントループの仕組みを理解するのに役立ちます。setTimeoutは非同期処理なので、1秒待つのではなく、1秒後に実行するようにスケジュールしてすぐに次の処理(処理3)に進みます。

非同期処理の種類

JavaScriptには主に3つの非同期処理の手法があります。

  1. コールバック関数: 伝統的な方法だが「コールバック地獄」の問題がある
  2. Promise: ES6で導入されたより構造化された方法
  3. async/await: ES2017で導入された同期処理のような見た目で非同期処理を書ける方法

この記事では概要のみ説明し、詳細は後の章で解説します。

JavaScriptのコールバック関数について

JavaScriptのPromiseについて

JavaScriptのasync/awaitについて

非同期処理のメリットとデメリット

次に非同期処理のメリットとデメリットを以下に列挙します。

メリット

  1. UIの応答性向上: 時間のかかる処理でもUIがフリーズしない
  2. 効率的なリソース利用: 待ち時間を他の処理に活用できる
  3. パフォーマンス向上: 複数の操作を並行して実行できる
  4. ユーザー体験の向上: 待ち時間を減らせる

デメリット

  1. コードの複雑化: 処理の流れが直感的でなくなる
  2. エラー処理の難しさ: エラーが発生した場所を追跡しにくい
  3. デバッグの難しさ: 実行順序がコードの記述順と異なる
  4. コールバック地獄: ネストが深くなり可読性が低下する(コールバック関数使用時)

非同期処理のベストプラクティス

  1. コールバック地獄を避ける: Promiseやasync/awaitを使用
  2. 適切なエラー処理: すべての非同期操作でエラーをキャッチ
  3. コードの可読性を考慮: 非同期処理がわかりやすいようにコメントを追加
  4. 並列処理を活用: 複数の独立した非同期処理は並行して実行
  5. 状態管理を明確に: どの処理が完了したか/していないか明確にする

非同期処理の歴史

JavaScriptの非同期処理は進化してきました。

  1. 初期: 単純なコールバック関数
  2. jQuery時代: Deferredオブジェクト
  3. ES6 (2015): Promiseの標準化
  4. ES2017: async/awaitの導入

この進化により、非同期処理はより書きやすく、読みやすくなっています。

非同期処理の実際の使用例

具体的に非同期処理はどのような場面で使われるのか使用例を用いて説明します。

1. データフェッチング

サーバーからのデータ取得を非同期で行います。UIをブロックせずに通信可能で、レスポンスを待つ間も他の処理を継続できます。Promiseやasync/awaitで効率的に処理します。

fetch() 関数とは

fetch() は、JavaScriptに標準で組み込まれている HTTP通信を行う非同期関数 です。
URLを指定してサーバーと通信し、データを取得したり送信したりできます。
非同期処理で動作するため、Promise を返します。

次のコードは、同期処理と非同期処理の違いを示しています。同期的な例ではデータ取得中に他の処理が止まる想定ですが、実際のfetch()は非同期で動作するためブロックされません。非同期的な例では、データ取得を待たずに他の処理を続行でき、通信完了後にthen()内の処理が実行されます。

// 同期的なアプローチ(現実的ではない例)
console.log("同期処理開始");

const data = fetch("https://jsonplaceholder.typicode.com/posts/1"); 
// ↑ 実際のfetchは非同期だが、ここでは「データ取得が完了するまで止まる」と仮定
console.log("データ取得完了:", data);
console.log("同期処理終了");

// 実際には、fetchは非同期で動作するため、上記のようにブロックすることはできません。


// 非同期的なアプローチ(現実的な例)
console.log("非同期処理開始");

fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then(response => response.json())
  .then(data => {
    console.log("データ取得完了:", data);
  });

console.log("この間も他の処理を続行可能");
console.log("非同期処理終了");

2. タイマー処理

setTimeoutやsetIntervalで指定時間後に処理を実行します。setTimeout()を使って3秒後に処理を実行する例です。プログラムは「開始」を表示した後、タイマーを設定してすぐに「終了」を表示します。その後3秒経過するとコールバック関数が呼ばれ、「3秒後」と出力されます。

console.log("開始");

setTimeout(() => {
  console.log("3秒後");
}, 3000);

console.log("終了");
// 結果: 開始 → 終了 → 3秒後

非同期処理により、待機中でも他の処理を続行できます。遅延実行や定期的なタスクを実現し、メインスレッドをブロックせずに時間制御が可能です。

3. イベントリスナー

addEventListener()でボタンのクリックイベントを監視し、クリックされたときにコールバック関数が実行され、「ボタンがクリックされました」と表示します。クリックされるまでは他の処理を止めずに実行を続けられる点が特徴です。

// Node.jsで実行できる擬似イベントリスナーの例

// 疑似的なボタンオブジェクトを定義
const button = {
  listeners: [],
  addEventListener(event, callback) {
    if (event === "click") {
      this.listeners.push(callback);
    }
  },
  click() {
    console.log("(クリックイベント発生)");
    this.listeners.forEach(callback => callback());
  }
};

// イベントリスナーを登録
button.addEventListener("click", () => {
  console.log("ボタンがクリックされました");
});

// クリックされるまで他の処理を続行
console.log("プログラム実行中...(3秒後にクリックをシミュレート)");

// 3秒後にクリックイベントを発生させる(非同期処理)
setTimeout(() => {
  button.click();
}, 3000);

ユーザー操作などのイベントを待機し、発生時に非同期的に処理します。複数のイベントを同時に監視でき、直感的なインタラクションを実現します。

非同期処理のよくある間違い

1. 実行順序の誤解

次のコードはfetchDataFromServer()はサーバーからデータを取得しますが、処理が完了する前に次の行が実行されるため、console.log(data)ではまだundefinedが表示されます。非同期処理では、結果を使う処理をコールバック関数の中で行う必要があります。

let data;

fetch("https://jsonplaceholder.typicode.com/posts/1") // ダミーAPIからデータ取得
  .then(response => response.json()) // レスポンスをJSON形式に変換
  .then(result => {
    data = result;
    console.log("コールバック内のdata:", data); // データ取得後に表示
  })
  .catch(error => {
    console.error("通信エラー:", error);
  });

console.log("直後のdata:", data); // 非同期処理中なのでundefinedが表示される

非同期処理はコードの記述順と実行順が一致しません。同期的な考え方でコードを書くと、想定外の実行順序でバグの原因となります。

2. ループ内での非同期処理

次のコードは、varによるスコープの影響を示しています。forループ内のiは関数スコープで共有されるため、ループ終了後のiは5になります。setTimeout()のコールバックはループ後に実行されるため、すべての出力が「5」となります。これを防ぐにはletを使うことでブロックスコープを作る方法が有効です。

for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i); // すべて5が出力される
  }, 100);
}

ループ内で非同期処理を行うと、すべての反復が並行して実行されます。ループ完了後に処理を行いたい場合は、Promise.allなどを使用する必要があります。

3. エラー処理の忘れ

次のコードは、非同期処理内でのエラーハンドリングの難しさを示しています。fetchDataFromServer()がコールバックを通じてデータを返しますが、processData(data)内でエラーが起きても通常のtry...catchでは捕捉できません。非同期処理では、エラーを正しく処理するためにコールバック内での明示的なエラーチェックやPromiseasync/await構文の利用が推奨されます。

// fetch()を使った非同期処理の例
fetch("https://jsonplaceholder.typicode.com/posts/1") // ダミーAPIからデータ取得
  .then(response => response.json()) // JSON形式に変換
  .then(data => {
    try {
      processData(data); // データ処理中にエラーが起きる可能性あり
    } catch (error) {
      console.error("処理中にエラーが発生:", error);
    }
  })
  .catch(error => {
    console.error("通信エラー:", error);
  });

非同期処理では従来のtry-catchが機能せず、未処理のPromise拒否が発生します。必ず.catch()やasync/awaitでのtry-catchによるエラーハンドリングが必要です。

まとめ

非同期処理はJavaScriptにおいて非常に重要な仕組みであり、コードの実行をブロックせず効率的に処理を進めることを可能にします。特にシングルスレッドで動作するJavaScriptでは不可欠な概念です。

これはイベントループとコールバックキューによって実現され、発展の過程でコールバック関数からPromise、さらにasync/awaitへと進化してきました。正しく活用すればパフォーマンスやユーザー体験を向上できますが、誤用するとバグや予期せぬ動作を引き起こすことがあります。

非同期処理は最初は理解が難しいかもしれませんが、JavaScript開発者として成長するためには避けて通れない道です。次の章では、非同期処理の具体的な実装方法である「コールバック関数」について詳しく学んでいきましょう。コールバック関数について

練習問題

問題1

以下のコードの実行結果を予想してください。

console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

console.log("C");

問題2

次の非同期処理の特徴について、正しいものには○、間違っているものには×をつけてください。

  1. 非同期処理を使うと、コードは常に書かれた順番に実行される ( )
  2. setTimeoutは非同期処理の一例である ( )
  3. 非同期処理はJavaScriptのシングルスレッドモデルの制約を克服するために必要である ( )
  4. 非同期処理を使うと、必ずプログラムの実行速度が速くなる ( )

問題3

以下のコードのどこに問題があるか指摘してください。

function getUserData(userId) {
  let user;
  fetchUserFromServer(userId, (data) => {
    user = data;
  });
  return user;
}

const data = getUserData(123);
console.log(data);

解答例

問題1解答例

実行結果:
A
C
B

理由:
JavaScriptはシングルスレッドで動作し、まず同期処理を実行する。
setTimeoutのコールバックは「タスクキュー」に入り、同期処理終了後に実行されるため、
“A” → “C” → “B” の順になる。

問題2解答例

非同期処理を使うと、コードは常に書かれた順番に実行される (×)
setTimeoutは非同期処理の一例である (○)
非同期処理はJavaScriptのシングルスレッドモデルの制約を克服するために必要である (○)
非同期処理を使うと、必ずプログラムの実行速度が速くなる (×)

問題3解答例

async function getUserData(userId) {
  const data = await fetchUserFromServer(userId);
  return data;
}

(async () => {
  const data = await getUserData(123);
  console.log(data);
})();

問題点:
fetchUserFromServerは非同期処理なのに、結果が返ってくる前に
getUserData関数がreturnしてしまうため、常にundefinedが返る。

修正案:
コールバックの中で処理を行う、またはPromise/async-awaitを使って非同期の完了を待つ。