MENU

JavaScript構文同期・非同期

【JavaScript】 forEachでawaitが使えないんですけど?

更新日:2021/05/28

 

forEach内でawaitを使用して非同期処理の終了を待つコードを作成したら、想定した動作になりませんでした。
どうしてなのか、その理由と解決方法を探っていきます。

 

まずはforEachを使用しない例

 

まずはforEachしないで、awaitを使用する例です。
次のデモスタートボタンを押してみてください。

 

 

 

1秒経過を待ってメッセージ出力。
その後2秒経過を待ってメッセージ出力と、時間の経過を待って次の処理を行っています。

 

次は、そのコード。

 

HTML


<button id="bt1">デモスタート</button>
<div id="output1"></div>

 

JavaScript

 


window.addEventListener( "DOMContentLoaded" , ()=> {
    const outputId = document.getElementById("output1");
    const output = t => outputId.innerHTML += t;

    const timer =  sec =>{
        return new Promise( resolve => {
            output(`<p>${sec}秒経過待ち</p>` );
            setTimeout( ()=>{
                resolve();
            },sec * 1000);
        });
    };

    const func = async ()=>{
        const seclist = [1,2,3];

        for( let i = 0; i < seclist.length ; i ++ ) {
            await timer( seclist[i] );
            output( `<p>${seclist[i]}秒経過しました</p>` );
        }

    };
    document.getElementById("bt1").addEventListener("click",func)

});

 

ボタンが押されて関数funcが呼び出されます。

 

関数funcは、forループを使って、timer関数を呼び出しています。
この時、timer関数でセットしたタイマーが終了するのを待ってから、次のループを処理しています。

 

ポイントは、funcがasync関数である点と、async関数内で処理待ちをしたい関数呼び出しにawaitを付加している点です。
詳しくは、次の記事を読んでみてください。
【JavaScript】 async/awaitの処理の流れを図で解説します

forEachを使ってみる

 

配列をforループで回すなんてダサい!

 

そんな声に答えて、forEachを使った方法に書き換えてみます。

 

forEachにかえてみた

 


const func = async ()=>{
       const seclist = [1,2,3];
 
       seclist.forEach( sec =>{ await timer(sec); } );
};

 

これを実行すると、次のエラーがでます。

 

Firefox : Uncaught SyntaxError: await is only valid in async functions and async generators

 

Chrome : Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules

 

つまり、「awaitはasync関数内でしか使えませんよ」と怒られてしまったのです。

 

でもちょっと待って。

 

func関数にasyncついてるから、ちゃんとasync関数内でawait使ってますよ?

 

どうしてエラーって言われてしまうのでしょうか?

 

冷静になって考えると、forEachの引数が関数だったりします。

 

sec =>{ await timer(sec); } ←関数

 

この関数はasync関数ではないので、awaitは使えないということなんですね。

 

ということで、secの前にasyncをつければ解決です。

 

asyncをつけてみた

 


const func = async ()=>{
       const seclist = [1,2,3];
 
       seclist.forEach( async sec =>{ await timer(sec); } );
};

 

早速実行してみましょう。

 

 

 

なんか違う!

 

最初のデモと動作が同じではありません。

 

またまた、どうしてでしょうか?

 

【JavaScript】 forEach/map/filter/reduceを根本的に理解するで、forEachの内部処理の要点をお伝えしています。

 

そこで紹介しているコードを、簡略化したのが次のコードです。

 


Array.prototype.forEach = function( callBack  ){

        const length = this.length;  
        for( let i = 0 ; i < length ; i ++ ){
                callBack( this[ i ] , i , this ); // コールバック呼び出し
        }
};

 

つまり、forEachは引数で受け取ったコールバック関数を、単純に配列の要素に対して呼び出しているだけということです。

 

コールバック関数の結果を考慮しませんし、ましてや非同期の解決待ちなんてしていません。

 

そのため、今回のタイマー待ちのコードは、引数1,2,3で3回timer関数を呼び出して終了です。

 

その後、timer関数内でセットしたタイマーが各コールバック関数を呼び出しています。

 

コールバック関数内でawaitは使用してはいけない

 

関数やメソッドにコールバック関数を渡す場合、async関数を渡すべきではありません。

 

async関数の実行結果はPromiseオブジェクトですが、多くのメソッドはこの実行結果を想定していないため、何らかの不具合がおこる可能性があるからです。

 

awaitで非同期の解決を待つ場合は、async関数内で使用しているかよく確認しましょう。

 

なお、次のように for-of を使用すると、forループでもカウンターを用意しなくて済みます。
おススメです。

 


const func = async ()=>{
    const seclist = [1,2,3];

    for( const sec of seclist){
        await timer(sec);
        output( `<p>${sec}秒経過しました</p>` );
    }
    
};

 

for-await-ofの利用

 

JavaScriptには for-await-of という構文があります。

 

Promiseの配列を上から順番に結果待ちしてくれる構文です。

 

for-await-ofについては、こちらをご覧ください。
【JavaScript】 for-await-of構文を理解してみる

 

これまでのコードを、for-await-ofに書き換えてみます。

 

for-await-ofに書き換えてみる

 


const timer =  sec =>{
    return new Promise( resolve => {
        output(`<p>${sec}秒経過待ち</p>` );
        setTimeout( ()=>{
            resolve(sec);
        },sec * 1000);
    });
};

const func = async ()=>{
    const seclist = [1,2,3];
    const timers = seclist.map( sec =>timer( sec ) );

    for await ( const sec of timers ){

        output( `<p>${sec}秒経過しました</p>` );
    }

};

 

関数funcで始めに、mapメソッドでtimer関数の結果を配列にしています。

 

その後、for-await-ofで、順番に結果待ちをしています。

 

なお、変数secにはtimer関数の結果がセットされます。
つまり、resolve(sec)のsecの値が、代入されます。

 

実行してみましょう。

 

 

 

失敗パターンでした。

 

mapでtimer関数を呼び出した段階で、タイマーがスタートしているので、こんな結果になります。

 

そこで、for-await-ofループでプロミスを取り出す時に、タイマーをスタートするように変更してみます。

 

ジェネレーター使ってみる

 


const timer =  function*(seclist) {
    for( const sec of seclist ){
        yield new Promise( resolve => {
            output(`<p>${sec}秒経過待ち</p>` );
            setTimeout( ()=>{
                resolve(sec);
            },sec * 1000);
        });
    }
};

const func = async ()=>{
    const seclist = [1,2,3];
    const timers = timer( seclist );

    for await ( const sec of timers){

        output( `<p>${sec}秒経過しました</p>` );
    }

};

 

timer関数をジェネレーター関数に変更してみました。

 

これで、for-await-ofがtimersにアクセスするたびに、タイマーがスタートするようになります。

 

ジェネレーター関数って何?という方は次の記事を読んでみてください。
【JavaScript】 Generator(ジェネレーター)とyieldってなんだ?関数とは何が違うのか

 

実行してみましょう。

 

 

 

うまくいきました。

けーちゃんおススメJavaScript入門書

  • スラスラ読める JavaScript ふりがなプログラミング
  • プログラム未経験者がJavaScript始めるならコレ!
    コードを掲載して自分で理解しろという投げっぱなしな入門書とは異なり、コードに一つ一つどんなことをやっているかをふりがなという形式で解説しています。
    それでいてJavaScriptの基礎と応用を学べる良書です。
  • これからWebをはじめる人のHTML&CSS、JavaScriptのきほんのきほん
  • JavaScriptの機能を実践で活かすにはHTMLやCSSの知識が不可欠です。
    しかしそれらの知識があることが前提として書かれている書籍が多い中、この本は総合的な知識を身に着けることができます。
    HTMLやCSSの知識も不安な方には、ぴったりの一冊です
  •  

    入門書の役割は、自分のやりたいことをネットで調べることができるようになるための、基礎的な知識の獲得です。
    まずはこれらの本でしっかりと基礎知識を身につけましょう。
    そしてもっと高度なことや専門的なことはネットで調べ、情報が足りないと感じたら書籍を購入してください。


    期間限定情報:
    6/21と6/22は年に一度のプライム会員大感謝祭!
    欲しかったアレが安く手に入るチャンスです
    忘れずにチェックしてください!
    僕は以前のタイムセール祭りで4Kモニタが買ったけど、それより安かったらどうしよう・・・

    ちなみにプライム会員でなくても、無料体験で参加できるようです。
    欲しい商品があるか、確認だけでもしておきましょう。

    記事の内容について

     

    こんにちはけーちゃんです。
    説明するのって難しいですね。


    「なんか言ってることおかしくない?」
    たぶん、こんなご意見あると思います。

    裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。

    そんなときは、ご意見もらえたら嬉しいです。

    ご意見はこちら。
    https://note.affi-sapo-sv.com/info.php

    【お願い】

    お願い

    ■このページのURL


    ■このページのタイトル


    ■リンクタグ


    ※リンクして頂いた方でご希望者には貴サイトの紹介記事を作成してリンクを設置します。
    サイト上部の問い合わせよりご連絡ください。
    ただしサイトのジャンルによっては、お断りさせていただくことがあります。