async/await 入門(JavaScript)

はじめに

今更ですが、JavaScriptのasync/awaitに関する備忘録になります。

  • 「今まで$.Deferred()Promiseなどで非同期処理は書いたことがあるが、async/awaitはわからない」
  • $.Deferred()Promiseなどの非同期処理の書き方より、もっと簡潔に書ける書き方があれば知りたい」
  • 「今までの非同期処理の書き方と比べて何が良いのかわからない」

といった人達向けの記事です。

$.Deferred()Promiseなどで非同期処理を書いたことがある前提のため、非同期処理自体に関する説明は記載しておりません。
記載している利用例のコードはChrome(最新)のコンソール上で動きますので、コンソール上で実行して動作を確認してみると理解が深まりやすいと思います。

本記事で用いている用語

Promiseを返す

Promiseオブジェクトを返すこと。

// Promiseを返す
return new Promise((resolve, reject) => {

});

Promiseの結果を返す

Promiseresolveもしくはrejectを実行すること。

return new Promise((resolve, reject) => {
    // Promiseの結果を返す
    resolve('resolve!!');
});

resolveする

Promiseresolveを実行すること。

return new Promise((resolve, reject) => {
    // succes!!をresolveする
    resolve('succes!!');
});

rejectする

Promiserejectを実行すること。

return new Promise((resolve, reject) => {
    // err!!をrejectする
    reject('err!!');
});

async/awaitとは

asyncawaitを利用した、非同期処理の構文のこと。

何故async/awaitを利用するのか

Promiseを利用した構文よりも、簡潔に非同期処理が書けるから。

async/awaitの対応状況

以下は各ブラウザのasync/awaitの対応状況。

ECMAScript 2016+ compatibility table
async-await.jpg

全てのブラウザで対応しているわけではないため、利用するならBabel等でトランスパイルする必要がある。

asyncとは

非同期関数を定義する関数宣言のこと。
以下のように関数の前にasyncを宣言することにより、非同期関数(async function)を定義できる。

async function sample() {}

async functionasyncで宣言した関数)は何をするのか

  • async functionは呼び出されるとPromiseを返す。
  • async functionが値をreturnした場合、Promiseは戻り値をresolveする。
  • async functionが例外や何らかの値をthrowした場合はその値をrejectする。

言葉だけだとわかりづらいため、利用例を見てみる。

async functionの利用例

以下はasync functionPromiseを返し、値をresolve、もしくはrejectしているか確認するための利用例。
※このように利用することはほとんどないと思いますが、async functionがどのような動きをしているのかを確認するために記載しております。

// resolve1!!をreturnしているため、この値がresolveされる
async function resolveSample() {
    return 'resolve!!';
}

// resolveSampleがPromiseを返し、resolve!!がresolveされるため
// then()が実行されコンソールにresolve!!が表示される
resolveSample().then(value => {
    console.log(value); // => resolve!!
});


// reject!!をthrowしているため、この値がrejectされる
async function rejectSample() {
    throw new Error('reject!!');
}

// rejectSampleがPromiseを返し、reject!!がrejectされるため
// catch()が実行されコンソールにreject!!が表示される
rejectSample().catch(err => {
    console.log(err); // => reject!!
});


// resolveErrorはasync functionではないため、Promiseを返さない
function resolveError() {
    return 'resolveError!!';
}

// resolveErrorはPromiseを返さないため、エラーが発生して動かない
// Uncaught TypeError: resolveError(...).then is not a function
resolveError().then(value => {
    console.log(value);
});

上記の通り、async functionPromiseを返し、値をresolve、もしくはrejectしていることがわかった。
上記はasync function単体の利用例だが、awaitと併用して利用することが多く、「asyncを利用するならawaitも必ず利用すべき」と書かれている記事もあった。

awaitとは

async function内でPromiseの結果(resolvereject)が返されるまで待機する(処理を一時停止する)演算子のこと。
以下のように、関数の前にawaitを指定すると、その関数のPromiseの結果が返されるまで待機する。

async function sample() {
    const result = await sampleResolve();

    // sampleResolve()のPromiseの結果が返ってくるまで以下は実行されない
    console.log(result);
}

awaitは何をするのか

  • awaitを指定した関数のPromiseの結果が返されるまで、async function内の処理を一時停止する。
  • 結果が返されたらasync function内の処理を再開する。

awaitasync function内でないと利用できないため、async/awaitの利用例を見ていく。

async/awaitの利用例

以下は単純なasync/awaitの利用例。

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value * 2);
        }, 2000);
    })
}

/**
 * sampleResolve()をawaitしているため、Promiseの結果が返されるまで処理が一時停止される
 * 今回の場合、2秒後にresolve(10)が返ってきてその後の処理(return result + 5;)が再開される
 * resultにはresolveされた10が格納されているため、result + 5 = 15がreturnされる
 */
async function sample() {
    const result = await sampleResolve(5);
    return result + 5;
}

sample().then(result => {
    console.log(result); // => 15
});

上記の処理をPromiseの構文で書くと以下のようになる。

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value * 2);
        }, 2000);
    })
}

function sample() {
    return sampleResolve(5).then(result => {
        return result + 5;
    });
}

sample().then(result => {
    console.log(result); // => 15
});

2つを見比べてみると、async/awaitの方が簡潔に書けることがわかる。
より複雑な処理の場合でもasync/awaitを利用した方が簡潔に書けるため、いくつか例を紹介していく。

連続した非同期処理

連続した非同期処理(Promise構文)

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 1000);
    })
}

function sample() {
    let result = 0;

    return sampleResolve(5)
        .then(val => {
            result += val;
            return sampleResolve(10);
        })
        .then(val => {
            result *= val;
            return sampleResolve(20);
        })
        .then(val => {
            result += val;
            return result;
        });
}

sample().then((v) => {
    console.log(v); // => 70
});

連続した非同期処理(async/await構文)

awaitを利用すれば、then()で処理を繋げなくても連続した非同期処理が書ける。

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 1000);
    })
}

async function sample() {
    return await sampleResolve(5) * await sampleResolve(10) + await sampleResolve(20);
}

async function sample2() {
    const a = await sampleResolve(5);
    const b = await sampleResolve(10);
    const c = await sampleResolve(20);
    return a * b + c;
}

sample().then((v) => {
    console.log(v); // => 70
});

sample2().then((v) => {
    console.log(v); // => 70
});

forを利用した繰り返しの非同期処理も書ける。

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 1000);
    })
}

async function sample() {
    for (let i = 0; i < 5; i += 1) {
        const result = await sampleResolve(i);
        console.log(result);
    }

    return 'ループ終わった。'
}

sample().then((v) => {
    console.log(v); // => 0
                    // => 1
                    // => 2
                    // => 3
                    // => 4
                    // => ループ終わった。
});

array.reduce()も利用できる。

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 1000);
    })
}

async function sample() {
    const array = [5, 10, 20];
    const sum = await array.reduce(async (sum, value) => {
      return await sum + await sampleResolve(value) * 2;
    }, 0);

    return sum;
}

sample().then((v) => {
    console.log(v); // => 70
});

連続の非同期処理は、処理を順番に行う必要がない限り利用すべきではないため注意。

例えば、画像を非同期読み込みをする場合、上記のような処理だと1つの画像を読み込みが完了するまで、次の画像の読み込みが始まらない

そのため、全ての画像の読み込みにかなりの時間がかかる。画像は連続して読み込む必要はないため、後述する並列の非同期処理で読み込むべき。

並列の非同期処理

並列の非同期処理(Promise構文)

function sampleResolve(value) {
  return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 2000);
    })
}

function sampleResolve2(value) {
  return new Promise(resolve => {
        setTimeout(() => {
            resolve(value * 2);
        }, 1000);
    })
}

function sample() {
    const promiseA = sampleResolve(5);
    const promiseB = sampleResolve(10);
    const promiseC = promiseB.then(value => {
        return sampleResolve2(value);
    });

    return Promise.all([promiseA, promiseB, promiseC])
        .then(([a, b, c]) => {
            return [a, b, c];
        });
}

sample().then(([a, b, c]) => {
    console.log(a, b, c); // => 5 10 20
});

並列の非同期処理(async/await構文)

Promise.allにもawaitを利用できるため、以下のように記述できる。

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 2000);
    })
}

function sampleResolve2(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value * 2);
        }, 1000);
    })
}

async function sample() {
    const [a, b] = await Promise.all([sampleResolve(5), sampleResolve(10)]);
    const c = await sampleResolve2(b);

    return [a, b, c];
}

sample().then(([a, b, c]) => {
    console.log(a, b, c); // => 5 10 20
});

array.map()も利用できる。

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 2000);
    })
}

async function sample() {
    const array =[5, 10, 15];
    const promiseAll = await Promise.all(array.map(async (value) => {
        return await sampleResolve(value) * 2;
  }));

    return promiseAll;
}

sample().then(([a, b, c]) => {
    console.log(a, b, c); // => 10 20 30
});

例外処理(エラーハンドリング)

例外処理(エラーハンドリング)Promise構文)

function throwError() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            try {
                throw new Error('エラーあったよ');
                resolve('エラーなかった');
            } catch(err) {
                reject(err);
            }
        }, 1000);
    });
}

function errorHandling() {
    return throwError()
        .then((result) => {
            return result;
        })
        .catch((err) => {
            throw err;
        });
}

errorHandling().catch((err) => {
    console.log(err); // => エラーあったよ
});

例外処理(エラーハンドリング)async/await構文)

awaitを利用すれば、非同期処理のtry catchを書ける。

function throwError() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            try {
                throw new Error('エラーあったよ')
                resolve('エラーなかった');
            } catch(err) {
                reject(err);
            }
        }, 1000);
    });
}

async function errorHandling() {
    try {
        const result = await throwError();
        return result;
    } catch (err) {
        throw err;
    }
}

errorHandling().catch((err) => {
    console.log(err); // => エラーあったよ
});

実はtry catchで囲まなくても上記のコードと同様に動く。

function throwError() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            try {
                throw new Error('エラーあったよ')
                resolve('エラーなかった')
            } catch(err) {
                reject(err);
            }
        }, 1000);
    });
}

async function errorHandling() {
    // throwErrorがrejectを返したら処理が中断される
    const result = await throwError();
    return result;
}

errorHandling().catch((err) => {
    console.log(err); // => エラーあったよ
});

終わり

フロントエンド実装で非同期処理は避けられないため、より簡潔に書けるasync/awaitを覚えておいて損はないと思います。
とはいえasync/awaitPromiseを利用しているため、Promise自体の理解は必須です。
理解を深めるためにも以下の一読をオススメします。

また、連続した非同期処理(for文などの中でawait)を利用するうえでの注意点に関しては、以下を一読すればさらに理解が深まるかと思います。

  • async/await地獄(単純に「使いどころを気をつけよう」という記事です。)

お知らせ

Udemy で webpack の講座を公開したり、Kindle で技術書を出版しています。

Udemy:
webpack 最速入門10,800 円 -> 2,400 円

Kindle(Kindle Unlimited だったら無料):
React 実践入門(800 円)
React Hooks 入門(500 円)

興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。

soarflat
フロントエンドエンジニア。Udemy で webpack の講座を公開しています。https://www.udemy.com/course/practical-webpack/?couponCode=ADC884F22BC2EA8DEE8D
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
この記事は以下の記事からリンクされています
NT1123Vue.jsでのasync,awaitの書き方からリンク
NT1123Javascript備忘録からリンク
TaHirokiJavaScript Promiseオブジェクトからリンク
onikan【JS】async,awaitを理解していくからリンク
過去の50件を表示する
コメント

@koheiiwamura
編集リクエストありがとうございます!!
リンクをアーカイブのものに変更しておきました。
ご指摘ありがとうございました:bow:

直前の Promise のエラーを補足するのは

function errorHandling() {
  return throwError()
    .then((result) => {
      return result;
    }, (err) => {
      console.log(err); // 何らかのエラー処理
      return err;
    });
}

とすべきかなと。
最後に catch() を書いてそこで throw をするのであれば、そもそも catch() を書く意味が無いというか…
最後に catch() を書く方法をとったにしても、then() が文字列を返すのであれば、catch() も文字列を返す方が良いのではないかなと。

良記事ありがとうございます。
1点どうしてもわからない部分が有リましたので、気が向いたら教えて頂けたら有り難いです。

async function sample() {
    const array = [5, 10, 20];
    const sum = await array.reduce(async (sum, value) => {
      return await sum + await sampleResolve(value) * 2;
    }, 0);

    return sum;
}

この4行目のsumの直前のawaitが必要な理由が分からないのです。
awaitの後ろにはPromiseを持ってくるものと理解していたのですが、sumみたいに数値を持ってくる事もあるのでしょうか。

よい記事ありがとうございます。
async/awaitについて、もやもやしていた部分がすっきりしました。
実は@ryounagaokaさんと同じ疑問を持ちました。
自分なりに調べました。
MDN web docsのArray.prototype.reduce()から引用します。
Array.prototype.reduleのcallback関数の第一引数について、

accumulator
callback の返値を蓄積するアキュームレーターです。これは、コールバックの前回の呼び出しで返された値、あるいは initialValue が指定されている場合はその値となります 。
(MDN web docsより)
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce

sumは前回のコールバック関数を実行した結果であり、このコールバック関数というのはasync関数のため、awaitする必要がある。と理解しました。

あなたもコメントしてみませんか :)
すでにアカウントを持っている方は
ユーザーは見つかりませんでした