JSの初心者にPromiseとasync/awaitの使い方を雑に説明してみる

2020.09.09

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、CX事業本部の夏目です。

先日、Javascriptの非同期処理に登場するPromiseとasync/awaitをJS初心者に説明する機会がありました。

なので、今回は説明したことを再度噛み砕いてから共有しようと思います。

Promiseの使い方

基本的な使い方

main.js

function sleep(sec) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`wait: ${sec} sec`);
            resolve(sec);
        }, sec * 1000);
    });
}

function main() {
    sleep(2).then((data) => {
        return sleep(data * 2);
    }).then((data) => {
        console.log(`sleep result: ${data}`)
        return new Promise((resolve, reject) => {
            resolve();
        });
    }).then((data) => {
        console.log(`empty call: ${data}`);
        return new Promise((resolve, reject) => {
            reject("failed");
        });
    }).then((data) => {
        console.log("not call 1");
    }).catch((e) => {
        console.log(`catch: ${e}`);
        return new Promise((resolve) => {
            resolve();
        });
    }).then((data) => {
        console.log(`empty call take2: ${data}`);
        return new Promise(() => {
            throw new Error("throwed");
        });
    }).catch((e) => {
        console.log(`failed: ${e}`);
        return new Promise((resolve) => {
            resolve()
        });
    }).catch((e) => {
        console.log("not call 2")
    }).finally(() => {
        console.log("finally");
    });
}

main();

実行結果

$ node main.js
wait: 2 sec
wait: 4 sec
sleep result: 4
empty call: undefined
catch: failed
empty call take2: undefined
failed: Error: throwed
finally

非同期処理を行いたい場合、Promiseのコンストラクタに渡す関数の中に記述します。

Promiseのコンストラクタに渡す関数は2つの引数を取ります。 第一引数resolve, 第二引数rejectはどちらも1個もしくは0個の引数をとる関数です。

resolveは非同期処理の成功時、rejectは失敗時に呼び出します。
注意すべきことは、これらはあくまで関数の呼び出しでしかないことです。
呼び出したからと言って、Promiseのコンストラクタに渡した関数が停止するわけではありません。

作成したPromiseオブジェクトに対して、基本的(?)に次の3つの関数を呼び出します。
また、これらの関数でメソッドチェーンを作って使います。

  • then((data) => {}): Promiseオブジェクトのコンストラクタに渡した関数でresolveが呼ばれたら、引数に渡した関数が実行される
  • catch((e) => {}): Promiseオブジェクトのコンストラクタに渡した関数でrejectが呼ばれたら、引数に渡した関数が実行される
  • finally(() => {}): メソッドチェーンで最後にかならず実行される

then()catch()は渡した関数でPromiseオブジェクトを渡せば、どんどんメソッドチェーンを伸ばすことができます。
複数の(非同期)処理を行うときなどチェーンを伸ばします。

catch()はそれよりも前の部分で起きたエラーを拾います。
これはrejectが呼ばれたものだけではなく、throwされたエラーであっても拾います。

Promise.all()

main.js

function sleep(sec) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`wait: ${sec} sec`);
            resolve(sec);
      }, sec * 1000);
    });
}

function main() {
    Promise.all([
        sleep(3),
        sleep(1),
        sleep(5),
        sleep(4),
        sleep(2)
    ]).then((result) => {
        console.log(result);
    });
}

main();

実行結果

wait: 1 sec
wait: 2 sec
wait: 3 sec
wait: 4 sec
wait: 5 sec
[ 3, 1, 5, 4, 2 ]

Promise.all()という関数を使うと複数の処理が終わるのを待つことができます。
then()に渡される値はPromise.all()に渡した順番と一致します。

async/awaitの使い方

main.js

function sleep(sec) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(`wait: ${sec} sec`);
            resolve(sec);
        }, sec * 1000);
    });
}

function raise() {
    return new Promise((_, reject) => {
        reject(new Error("failed"));
    });
}

async function sum(a, b) {
    return a + b;
}

async function main() {
    const sec = await sleep(2);
    console.log(`result: ${sec} sec`);
    try {
        await raise();
    } catch (e) {
        console.log(`error: ${e}`);
    }
    const not_await = sum(1, 2);
    console.log(`not await: ${not_await}`);
    const did_await = await sum(2, 3);
    console.log(`did await: ${did_await}`);
}

main()

実行結果

$ node main.js
wait: 2 sec
result: 2 sec
error: Error: failed
not await: [object Promise]
did await: 5

async/awaitというのはPromiseをもっと便利に扱うための機構です。

asyncと修飾した関数内でPromiseオブジェクトにawaitをつけることで、コンストラクタに渡した処理が完了するまで待ちます。
resolveに渡した内容が返り値に、rejectに渡した内容はthrowされます。

また、asyncと修飾した関数はreturnで値を返していてもPromiseオブジェクトでラップされます。
そのため、awaitをつけることでreturnされた値を取得することができます。

まとめ

以上、Promiseとasync/awaitの使い方でした。

よく使うけどわかりにくい機能なので、理解の一助になれば幸いです。

雑に説明しただけなので、詳しく知りたいときは次のドキュメントなどで確認してください。

おまけ) 今は昔、Callback地獄といふものありけり

main.js

function sleep(sec, callback) {
    setTimeout(function() {
        console.log("wait: " + sec + " sec");
        callback(sec);
    }, sec * 1000);
}

function main() {
    var sec0 = 1;
    sleep(sec0, function(sec1) {
        sleep(sec0 + sec1, function(sec2) {
            sleep(sec1 + sec2, function(sec3) {
                sleep(sec2 + sec3, function(sec4) {
                    sleep(sec3 + sec4, function(sec5) {
                        console.log("last wait time: " + sec5 + "sec");
                    });
                });
            });
        });
    });
}

main();

実行結果

wait: 1 sec
wait: 2 sec
wait: 3 sec
wait: 5 sec
wait: 8 sec
last wait time: 8sec