54
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptのforEachでawaitが効かない理由

Last updated at Posted at 2025-07-08

背景

JavaScriptでは forEach という配列から要素を取り出して反復処理できる関数があります。
前の記事 JavaScriptのforEach内でbreakができない理由【備忘録】 では、 forEach の中では break が使えず、途中でループを抜けることができない理由についてまとめました。

今回はその続きとして、forEach の内部で await を使った場合に、非同期処理の完了を待たずに次の処理へ進んでしまう、という挙動について理由を調べてみました。

forEach内でawaitしてみる

forEach の中で await して得られた値を配列に追加し、 forEach の直後に配列を console.log で出力する例で確認します。

for文の感覚でいえば、イテレーション中の await で都度処理を待ってくれるイメージなので、全ての要素への処理が終わったら配列の各要素が出力されると予想できます。

// 楽器の名前を文章に変換する非同期関数
function describeInstrument(instrument) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`${instrument} は素晴らしい楽器です!`)
    }, 1000)
  })
}

const instruments = ['ドラム', 'スラップフォン']
const descriptions = []

instruments.forEach(async item => {
  const result = await describeInstrument(item)
  descriptions.push(result)
})

console.log('forEach終了後のdescriptions: ', descriptions)
// 実行結果:
//   forEach終了後のdescriptions: []

結果は、forEach 内で await してから配列に値を追加しているのにも関わらず、直後のconsole.log では空の配列が出力されています。

awaitについて(前提知識)

await は、 Promise を返す関数の非同期処理が完了するのを待ち、その結果の値を取得するために使います。
非同期処理が完了するまでは該当の関数の実行は一時停止され、その間は他の処理(UI の更新や他の関数の実行など)ができます。

await と Promise の関係

Promise を返す関数は、実行された直後にはまだ結果がわからないため、まず「未解決の Promise(Pending 状態)」を返します。
その Promise が解決(resolve)されるとawait は結果の値を受け取り、中断された関数が再開されます。

await しない場合は非同期処理の解決を待たないため、未解決の Promise のみ返却されて処理が進みます。

forEachの定義(前提知識)

for文は構文として定義されていますが、forEach はメソッドです。

ECMA-262forEach の仕様では、対象の配列をループさせて forEach 内に書いた関数をコールバック関数として呼び出しています。

1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. If IsCallable(callback) is false, throw a TypeError exception.
4. Let k be 0.
5. Repeat, while k < len,
    a. Let Pk be ! ToString(𝔽(k)).
    b. Let kPresent be ? HasProperty(O, Pk).
    c. If kPresent is true, then
        i. Let kValue be ? Get(O, Pk).
        ii. Perform ? Call(callback, thisArg, « kValue, 𝔽(k), O »).
    d. Set k to k + 1.
6. Return undefined.

なぜ forEach は非同期処理を待ってくれないのか

forEachPromise を返すコールバック関数の実行を待ってくれない理由は、 forEach のループ内でコールバック関数を await していないためです。

awaitについて で述べたように、Promise を返すコールバック関数を呼び出すと、一旦未解決の Promise が返ってきます。

forEachの定義 を確認すると、forEach 内ではコールバック関数をループの中で呼び出していますが、 await がないため、Promise の解決を待たないままループが進行します。

これにより forEach はコールバック関数の結果を待たずに処理を続行してしまいます。

補足

ただし、Promise の結果を forEach のループ内で待ってくれないだけで、コールバック関数の処理自体は実行されています。
そのため、十分な時間を待った後にコールバック関数内で更新された配列を確認すると、値が入っていることが確認できます。

function describeInstrument(instrument) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`${instrument} は素晴らしい楽器です!`)
    }, 1000)
  })
}

const instruments = ['ドラム', 'スラップフォン']
const descriptions = []

instruments.forEach(async item => {
  const result = await describeInstrument(item)
  descriptions.push(result)
})

// Promise が解決する前の出力
console.log('forEach終了後のdescriptions:',descriptions)

// Promise が解決した後の出力
setTimeout(() => {
  console.log('3秒後のdescriptions:', descriptions)
}, 3000)
// 実行結果:
//   forEach終了後のdescriptions: []
//   3秒後のdescriptions: ["ドラム は素晴らしい楽器です!", "スラップフォン は素晴らしい楽器です!"]

終わりに

forEach 内で await をすると、仕様により非同期処理の完了を待たずに処理が進んでしまうことがわかりました。
今回は forEach 内の awaitで非同期処理の完了を待たない理由に焦点を当てましたが、for...of構文を使うことで、配列の各アイテムを非同期処理することができます。
forEach だけでなく普段何気なく使っている関数やメソッドを使う際にも注意したいと思います。

54
49
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?