async/awaitとは

ES2015ではPromiseオブジェクトが導入され、非同期処理を簡潔に扱えるようになりました。
Promiseが導入される前は、非同期処理を扱うためにはコールバック処理を書いていました。例えば、「ex1.jsのファイルの読み込みを開始する ⇒ 読み込み完了 ⇒ ex2.jsのファイルの読み込みを開始する ⇒ 読み込み完了 ⇒ ...」といった流れで、直前のアクションが終了したら次のアクションを実行します。しかし、これには「コールバック地獄」の問題があり、その解決策としてPromiseオブジェクトが導入されたという経緯があります。

しかし、このPromiseオブジェクトは直感的に扱うのが難しいという問題がありました。

ES2017では、これを解決するためにasyncとawaitという文法が追加され、非同期処理がより簡潔に記述できるようになりました。asyncとawaitはいわゆる「シンタックスシュガー」で裏側ではPromiseオブジェクトを使用して機能が実現されています。

ちなみにasyncはasynchronousの略で「非同期」という意味です。awaitは文字通り「待つ」という意味です。

async

asyncはfunction(アロー関数)の前に記述します。asyncが記述された関数は同期関数から非同期関数に変化します。

関数は常にPromiseを返します。コード中にPromiseを返さないreturnがある場合、JavaScriptは自動的にその値を解決(resolve)されたPromiseにラップします。

つまり、次の全てのコードは同じ振る舞いをします。

async function func() {
    return 1;
}

func().then((num) => {
    console.log(num); // 1
});
function func() {
    return Promise.resolve(1);
}

func().then((num) => {
    console.log(num); // 1
});
function func() {
    return new Promise((resolve, reject) => {
        resolve(1);
    });
}

func().then((num) => {
    console.log(num); // 1
});

エラー処理

解決した場合のresolve(1)return 1と書くことができますが、拒否の場合はreject(new Error("失敗!"))throw new Error("失敗!")と書くことができます。

つまり、次の全てのコードは同じ振る舞いをします。

async function func() {
    throw new Error("失敗!");
}

func().catch((err) => {
    console.log(err); // Error: 失敗! ...
});
async function func() {
    //Promise.reject()には時間がかかるのでawaitしています
    await Promise.reject(new Error("失敗!"));
}

func().catch((err) => {
    console.log(err); // Error: 失敗! ...
});
async function func() {
    return new Promise((resolve, reject) => {
        reject(new Error("失敗!"));
    });
}

func().catch((err) => {
    console.log(err); // Error: 失敗! ...
});

エラーはメソッドチェーンの.catch(...)以外にもtry..catchでキャッチすることができます。

async function func() {

  try {
    throw new Error("失敗!");
  } catch(err) {
    console.log(err); // Error: 失敗! ...
  }
}

func();

具体的な使い分けとしては、コードの最上位にいるときはasyncが使えないため、then..catchで処理します。メソッドの中にいるときはasyncとawaitが使えるのでtry..catchで処理します。

await

awaitは、asyncが付いた関数の中でのみ有効です。これは非同期関数の実行を一時停止し、Promiseが解決するまで待機させます。

async function func() {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve("done!"), 2000)
    });

    let result = await promise; // Promiseが解決するまで待機します
    console.log(result); // "done!"
}

func();

async/awaitで非同期処理を書いてみる

Fetch APIを使用したjsonの取得処理をasync/awaitで書いてみます。

まず初めにこの処理をPromiseで書く場合には次のようになります。これを後でasync/awaitに書き換えます。

fetch()はPromiseを返します。

function loadJson(url) {
    return fetch(url).then(response => {
        if (response.status == 200) {
            return response.json();
        } else {
            throw new Error(response.status);
        }
    });
}

const url = 'fetch_async_await.json';

loadJson(url).catch(console.log);

Fetch APIを使用するための準備を行います。今回はNode.jsでWebサーバを用意します。
次のコマンドでhttp-serverをグローバルインストールしておいてください。

npm install -g http-server

適当なディレクトリを作成し、その中に次のファイルを作成してください。

fetch_async_await.json

{ "name": "chappie", "age": 6 }

fetch_async_await.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <script src="fetch_async_await.js"></script>
    </body>
</html>

fetch_async_await.js

async function loadJson(url) {
    const response = await fetch(url);

    if (response.status === 200) {
        // response.json()もPromiseを使用しているため、awaitします。
        const json = await response.json();
        return json;
    }

    throw new Error(response.status);
}

const url = 'fetch_async_await.json';

loadJson(url).catch(console.log);

ターミナル(コマンドプロンプト)でファイルを作成したディレクトリの中に移動し、http-serverを実行してください。その後、ブラウザでhttp://localhost:8080/fetch_async_await.htmlにアクセスして動作を確認してください。

並列化

各Promiseを並列に実行するにはPromise.allを使用します。fetch()もPromiseを返却するので同様です。
複数のPromiseを待つ必要があるとき、Promise.allでラップしてからawaitします。
最初に失敗したPromiseからの例外はPromise.allに伝播するので、try..catchで拾うことができます。

async function main() {
    try {
        let results = await Promise.all([
            fetch("http:// ..."),
            fetch("http:// ..."),
            ...
        ]);
    } catch(err) {
        console.log(err);
    }
}

main();

async/awaitで非同期処理を並列に実行してみる

Fetch APIとasync/awaitを使用してJSONファイルを複数取得する処理を並列実行するように書いてみます。
今回はNode.jsのhttp-serverを使用するので、npmから以下のコマンドでインストールしておいてください。

npm install -g http-server

適当なディレクトリを作成し、その中に次のファイルを作成してください。

promise_all_1.json

{ "name": "Banana", "value": 150 }

promise_all_2.json

{ "name": "orange", "value": 100 }

promise_all.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <script src="promise_all.js"></script>
    </body>
</html>

promise_all.js

async function getAllJson(urls) {
    const results = Promise.all(
        urls.map(async url => {
            try {
                const response = await fetch(url);
                if(response.status === 200) {
                    const json = response.json();
                    return json;
                }
                
                throw new Error(response.status);
            } catch(err) {
                console.log(err);
            }
        })
    );
    return results;
}

const urls = [
    "promise_all_1.json",
    "promise_all_2.json",
];

getAllJson(urls).then(feeds => {
    feeds.forEach(feed => {
        console.log(feed);
    });
});

ターミナル(コマンドプロンプト)でファイルを作成したディレクトリの中に移動し、http-serverを実行してください。その後、ブラウザでhttp://localhost:8080/promise_all.htmlにアクセスして動作を確認してください。

補足

以下のように書くこともできます。

async function main() {
    try {
        const promiseResults = await Promise.all([
            fetch("promise_all_1.json"),
            fetch("promise_all_2.json"),
        ]);

        const results = await Promise.all(
            promiseResults.map(pr => pr.json())
        );

        results.forEach(feed => {
            console.log(feed);
        });
    } catch (err) {
        console.log(err);
    }
}

main();

参考
async function - JavaScript | MDN
Async/await