async/awaitの覚え書き

May 14, 2020

この記事ではJavascriptのasync/awaitについて説明します。

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

SmokyDog

I love dog and web.

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.