カテゴリ: JavaScript 更新日: 2026/02/16

JavaScriptのループと非同期処理の注意点を解説!初心者でもわかる使い方と落とし穴

JavaScriptのループと非同期処理の注意点を解説
JavaScriptのループと非同期処理の注意点を解説

先生と生徒の会話形式で理解しよう

生徒

「先生、JavaScriptでループを使って非同期の処理をするときに注意することはありますか?」

先生

「はい、JavaScriptのループと非同期処理は一緒に使うとちょっと難しい部分があります。特に順番やタイミングに気をつける必要がありますよ。」

生徒

「具体的にはどんな問題が起きるんですか?」

先生

「それでは、基本の仕組みとよくある注意点をわかりやすく説明しますね!」

1. JavaScriptの非同期処理とは?

1. JavaScriptの非同期処理とは?
1. JavaScriptの非同期処理とは?

非同期処理(ひどうきしょり)とは、「ある処理が終わるのを待たずに、次の処理を進めること」を言います。たとえば、ネットからデータを取ってくる処理は時間がかかるので、待っている間に他の処理を先に進められる仕組みです。

JavaScriptでは、この非同期処理を使ってプログラムの動きをスムーズにしています。

2. ループ処理と非同期処理が一緒に使われるときの問題

2. ループ処理と非同期処理が一緒に使われるときの問題
2. ループ処理と非同期処理が一緒に使われるときの問題

普通のループ処理は順番に1つずつ処理をしますが、非同期処理は終わる順番がバラバラになります。これが原因で、思った通りの順番で結果が得られなかったり、変数の値が期待と違ったりすることがあります。

たとえば、forループの中で非同期の関数を使うときに注意が必要です。

3. よくある問題例:変数のスコープと非同期処理

3. よくある問題例:変数のスコープと非同期処理
3. よくある問題例:変数のスコープと非同期処理

次の例を見てください。ループで変数iを使って非同期処理を呼んでいますが、意図した値が出ません。


for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 100);
}

実行結果は「3」「3」「3」となります。なぜでしょうか?

これは、varで宣言した変数のスコープ(有効範囲)が関係しています。varはループの外でも同じ変数を使うため、ループ終了後の値「3」がすべての非同期処理で使われてしまうのです。

4. 解決方法① letを使う

4. 解決方法① letを使う
4. 解決方法① letを使う

letはブロックスコープ({}内だけ有効な変数)なので、ループの各回ごとに別の変数として扱われます。これで問題を解決できます。


for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 100);
}

実行結果は「0」「1」「2」となり、期待通りです。

5. 解決方法② 即時関数(IIFE)を使う

5. 解決方法② 即時関数(IIFE)を使う
5. 解決方法② 即時関数(IIFE)を使う

古いJavaScriptでも対応できる方法として、即時実行関数(IIFE)があります。ループの中で即座に関数を呼び出して変数の値を閉じ込めます。


for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => {
      console.log(j);
    }, 100);
  })(i);
}

これも「0」「1」「2」と表示されます。

6. async/awaitでループの非同期処理を順番に実行する

6. async/awaitでループの非同期処理を順番に実行する
6. async/awaitでループの非同期処理を順番に実行する

JavaScriptのasyncawaitは、非同期処理を順番に書くときに便利です。awaitは非同期処理の完了を待つ意味です。


async function processArray(arr) {
  for (const item of arr) {
    await waitAndLog(item);
  }
}

function waitAndLog(value) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(value);
      resolve();
    }, 100);
  });
}

processArray([10, 20, 30]);

この例では、配列の値を順番に1つずつログに出しています。非同期でも順番が守られます。

7. 注意点:forEachはawaitと相性が悪い

7. 注意点:forEachはawaitと相性が悪い
7. 注意点:forEachはawaitと相性が悪い

forEachは内部で処理を繰り返すため、awaitを使っても全ての非同期処理が同時に走ってしまい、順番に処理できません。


const arr = [1, 2, 3];
arr.forEach(async (num) => {
  await waitAndLog(num);
});

順番に実行したい場合は、for...ofループを使いましょう。

8. まとめなしで最後にひとこと

8. まとめなしで最後にひとこと
8. まとめなしで最後にひとこと

JavaScriptのループと非同期処理は、一緒に使うときに変数のスコープや処理の順番に注意が必要です。letasync/awaitを上手に使って、正しく動くコードを書きましょう。

まとめ

まとめ
まとめ

今回の記事では、JavaScript開発において多くの初心者が、そして時には中級者以上であっても陥りやすい「ループ処理と非同期処理の組み合わせ」について詳しく解説してきました。Webフロントエンド開発やNode.jsを用いたサーバーサイド開発において、APIからのデータ取得やファイルの入出力、タイマー処理といった非同期処理は避けて通れない要素です。それらを繰り返し処理の中で扱う際には、JavaScriptの基本的な動作原理であるイベントループやスコープの概念を正しく理解しておくことが、バグを防ぐための最短ルートとなります。

非同期処理における変数の挙動と最新の対策

かつて主流だったvar宣言による変数管理は、関数スコープという性質上、非同期処理のコールバックが実行される頃にはループが終了し、最終的な値のみが参照されてしまうという深刻な問題を引き起こしていました。しかし、現代のJavaScript(ES6以降)ではletconstを用いることでブロックスコープが適用され、各ループの反復ごとに新しい変数の状態が保持されるようになっています。これにより、特別な細工をせずとも直感的なコードが書けるようになりました。

また、より高度な制御が必要な場合には、Promiseを同期的な見た目で記述できるasync/awaitが非常に強力です。特に「順番に1つずつ処理を終わらせたい(直列処理)」のか、「一斉に処理を開始して効率を上げたい(並列処理)」のかによって、構文を使い分ける必要があります。

実践的なサンプルプログラム:直列処理と並列処理

ここで、実際に開発現場でよく使われるテクニックをコードで振り返ってみましょう。まずは、配列の要素を1つずつ順番に処理する「直列実行」のパターンです。


/**
 * データの配列を1つずつ順番にAPI送信するようなシミュレーション
 */
async function uploadSequentially(dataList) {
    console.log("一括アップロードを開始します(直列)...");
    
    for (const item of dataList) {
        // 非同期処理の完了を待機する
        const result = await simulateApiCall(item);
        console.log(`処理完了: ${result}`);
    }
    
    console.log("すべてのアップロードが完了しました。");
}

function simulateApiCall(value) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`データ [${value}]`);
        }, 500);
    });
}

const targetData = ["画像1", "画像2", "画像3"];
uploadSequentially(targetData);

上記のコードを実行すると、各データが0.5秒ごとに1つずつ出力されます。一方で、処理速度を重視して一斉に実行したい場合は、以下のようにPromise.allを活用するのがベストプラクティスです。


/**
 * データの配列を同時に全て送信するシミュレーション(並列)
 */
async function uploadInParallel(dataList) {
    console.log("一括アップロードを開始します(並列)...");
    
    // mapを使ってPromiseの配列を作成
    const promises = dataList.map(item => simulateApiCall(item));
    
    // 全てのPromiseが解決されるのを待つ
    const results = await Promise.all(promises);
    
    console.log("実行結果一覧:", results);
    console.log("すべてのアップロードが完了しました。");
}

uploadInParallel(targetData);

実行結果は以下のようになります。


一括アップロードを開始します(並列)...
(約0.5秒の待機の後)
実行結果一覧: ["データ [画像1]", "データ [画像2]", "データ [画像3]"]
すべてのアップロードが完了しました。

さらなる応用:TypeScriptでの型定義

実務ではTypeScriptを使用して、より堅牢なコードを書く機会も多いでしょう。非同期ループを関数に切り出す際は、返り値の型をPromise<void>などに指定することを忘れないようにしましょう。


interface UserTask {
    id: number;
    title: string;
}

async function executeUserTasks(tasks: UserTask[]): Promise<void> {
    for (let i: number = 0; i < tasks.length; i++) {
        const task: UserTask = tasks[i];
        await new Promise<void>((resolve) => {
            setTimeout(() => {
                console.log(`タスクID ${task.id}: ${task.title} を実行中...`);
                resolve();
            }, 300);
        });
    }
}

const myTasks: UserTask[] = [
    { id: 101, title: "メール送信" },
    { id: 102, title: "バックアップ作成" }
];

executeUserTasks(myTasks);

まとめとしての最終アドバイス

JavaScriptにおける非同期処理は、最初は戸惑うことが多いポイントです。しかし、「現在のループの状態を維持したいならletを使う」「順番を守りたいならfor...ofの中でawaitする」「一斉にやりたいならPromise.allを使う」という3つの原則を覚えておくだけでも、多くのトラブルを回避できます。

特にforEach内でのawaitが機能しない(意図せず並列に動いてしまう)という仕様は、現場でもよく見かける落とし穴です。常に「今、自分のコードは待機しているのか、それとも流し読みされているのか」を意識しながらコーディングを進めてみてください。

先生と生徒の振り返り会話

生徒

「先生、ありがとうございました!varを使っていた頃に起きていた『全部同じ数字になっちゃう現象』の理由がやっと分かりました。スコープって本当に大事なんですね。」

先生

「そうですね。昔のJavaScriptではクロージャや即時関数(IIFE)を使って苦労して解決していたんですよ。今はletがあるおかげで、随分と書きやすくなりましたね。」

生徒

「あと、forEachの中でawaitを使っても順番待ちをしてくれないというのは驚きました。配列を回すときはいつもforEachを使っていたので、これからは使い分けに気をつけます。」

先生

「素晴らしい気づきです!基本的にはfor...ofを使えば間違いありません。もしパフォーマンスを求めて並列で一気に終わらせたいときは、mapPromise.allを組み合わせてみてください。これで非同期マスターに一歩近づきましたね!」

生徒

「はい!コードの書き方一つで、効率もバグの少なさも変わるんだと実感しました。これからも非同期処理の仕組みを意識して練習してみます!」

関連記事:
カテゴリの一覧へ
新着記事
New1
JavaScript
JavaScriptの条件式におけるnull・undefined判定のコツ!初心者でもわかる判定方法ガイド
New2
JavaScript
JavaScriptの条件式で論理演算子(&&, ||, !)を活用する方法!初心者でもわかる使い方ガイド
New3
TypeScript
TypeScriptでエラー処理を共通関数化して効率的に開発しよう!初心者向け例外処理ガイド
New4
JavaScript
JavaScriptの条件式における比較演算子の使い方まとめ!初心者でもわかる基本ルールと活用法
人気記事
No.1
Java&Spring記事人気No1
JavaScript
JavaScriptのインストール方法まとめ!Windows・Mac・Linux別にステップ解説
No.2
Java&Spring記事人気No2
JavaScript
JavaScriptのtoStringとString関数の違いを初心者向けに解説
No.3
Java&Spring記事人気No3
TypeScript
TypeScriptでコメントを書く正しい書き方と使い分け【初心者向けにやさしく解説】
No.4
Java&Spring記事人気No4
JavaScript
JavaScriptで統一感のあるコードを書くための基本ルール完全ガイド初心者向けコーディング規約入門
No.5
Java&Spring記事人気No5
JavaScript
JavaScriptプログラムの実行方法まとめ!ブラウザ・Node.js・コンソールの使い方
No.6
Java&Spring記事人気No6
JavaScript
JavaScriptのonclick・onchangeなどの基本イベントを理解しよう
No.7
Java&Spring記事人気No7
TypeScript
TypeScriptのnever型を使ったエラーハンドリング!例外処理を極める安全なコードの書き方
No.8
Java&Spring記事人気No8
JavaScript
JavaScriptの数値フォーマット(小数点以下の桁数調整)を学ぼう