2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScript の Promise / async / await をおさらいした

Posted at

はじめに

趣味のプログラミングで JavaScript を使い始めて随分になります。ところが、非同期処理や Promise になかなか慣れません。そろそろきちんと使えるようにしておきたいと思いました。

非同期関数を順に呼出する

例えば、3 つのファイルを順に読込して処理したいとします。以下のコードを書きます。

import fs from 'fs';

fs.readFile("ファイル1", "utf-8", (err, data) => {
    console.log("readFile (1) finished.");
});

fs.readFile("ファイル2", "utf-8", (err, data) => {
    console.log("readFile (2) finished.");
});

fs.readFile("ファイル3", "utf-8", (err, data) => {
    console.log("readFile (3) finished.");
});

fs.readFile() は非同期関数であるため、「ファイル1」の読込が終わってから「ファイル2」続いて「ファイル3」の読込・・となりませんね。順に読込するにはどうしたらよかったでしょうか。

コールバックで対応する

最初の呼出のコールバックで次の呼出して、そのコールバックで次の呼出して・・とします。

fs.readFile("ファイル1", "utf-8", (err, data) => {
    console.log("readFile (1) finished.");

    fs.readFile("ファイル2", "utf-8", (err, data) => {
        console.log("readFile (2) finished.");

        fs.readFile("ファイル3", "utf-8", (err, data) => {
            console.log("readFile (3) finished.");
        });
    });
});

順に実行したい処理が増えると、コールバックのネストが深くなり、読みづらくなる欠点が指摘されていますね。

async.js で対応する

サードパーティのライブラリ async.js (2012 年に公開)を使います。

caolan/async: Async utilities for node and the browser

import async from 'async';

async.series([
    (done) => {
        fs.readFile("ファイル1", "utf-8", (err, data) => {
            console.log("readFile (1) finished.");
            done(err, null);
        });
    },
    (done) => {
        fs.readFile("ファイル2", "utf-8", (err, data) => {
            console.log("readFile (2) finished.");
            done(err, null);
        });
    },
    (done) => {
        fs.readFile("ファイル3", "utf-8", (err, data) => {
            console.log("readFile (3) finished.");
            done(err, null);
        });
    }
], (err, results) => {
    if (err) console.log(err);
});

コード量は増えてしまいましたが、順に実行するのが分かりやすく書けています。

jQuery で対応する

サードパーティのライブラリ jQueryDeferred が用意されました(2011 年)。

後述する Promise が用意されたため、今になって Deferred を使うことはないでしょう。

Promise で対応する

JavaScript 言語に Promise が用意されました(2015 年)。これを使います。

new Promise((resolve, reject) => {
    fs.readFile("ファイル1", "utf-8", (err, data) => {
        console.log("readFile (1) finished.");
        resolve();
    });
})
.then((resolve, reject) => {
    return new Promise((resolve, reject) => {
        fs.readFile("ファイル2", "utf-8", (err, data) => {
            console.log("readFile (2) finished.");
            resolve();
        });
    });
})
.then((resolve, reject) => {
    return new Promise((resolve, reject) => {
        fs.readFile("ファイル3", "utf-8", (err, data) => {
            console.log("readFile (3) finished.");
            resolve();
        });
    });
});

最初の呼出と以降の呼出のネストが違うのが気になりますね。↑
以下のようにしてもいいようです。↓

Promise.resolve()
.then(() => {
    return new Promise((resolve, reject) => {
        fs.readFile("ファイル1", "utf-8", (err, data) => {
            console.log("readFile (1) finished.");
            resolve();
        });
    });
})
.then((resolve, reject) => {
    return new Promise((resolve, reject) => {
        fs.readFile("ファイル2", "utf-8", (err, data) => {
(以下略)

非同期関数を順に呼出できたけれど

非同期関数を順に呼出できるようになりました。ところが、注意しないといけない点があります。

new Promise((resolve, reject) => {
    fs.readFile("ファイル1", "utf-8", (err, data) => {
        console.log("readFile (1) finished.");
        resolve();
    });
})
(中略)
.then((resolve, reject) => {
    return new Promise((resolve, reject) => {
        fs.readFile("ファイル3", "utf-8", (err, data) => {
            console.log("readFile (3) finished.");
            resolve();
        });
    });
});

console.log("all functions finished.")

実行すると

all functions finished.
readFile (1) finished.
readFile (2) finished.
readFile (3) finished.

呼出の後に書かれた処理が先に実行されます。↑
new Promise() ~ then() は、関数を呼出して、終了したときの処理を予約します。予約し終えると終了を待たずに、次の処理に移るわけです。

非同期関数を呼出する

関数を順に実行するだけなのに、随分と面倒なことをしないといけませんね。

同期関数なら、こんな面倒なことはありません。非同期関数は、なぜ欲しいのでしょう。

前述の fs.reafFile() は、同期処理できる fs.readFileSync() が用意されています。これを使うと

const data = fs.readFileSync("ファイル", "utf-8");
console.log("readFileSync finished.");

これを入力画面から呼出すとします。(以下のコードはイメージです。)

●●●●.addEventListner('○○○○', () => {
    const data = fs.readFileSync("ファイル", "utf-8");
    console.log("readFileSync finished.");
});

ファイルが大きくて読込に時間が掛かると、これを呼出した入力画面は、処理が終わるまでフリーズします。プログラムがハングアップしたように見えるでしょう。
これを解消するために、非同期で処理される関数を呼出するようにします。

●●●●.addEventListner('○○○○', () => {
    fs.readFile("ファイル", "utf-8", (err, data) => {
        console.log("readFile finished.");
    });
});

呼出自体は直ぐに完了するので、呼出した側の入力画面の制御に戻ってきます。

非同期関数を作成する

fs.readFile() のように非同期処理できる関数を、作成できるでしょうか。

まず、同期処理する関数を作成して呼出してみます。

function funcSync() {
    時間の掛かる処理
    const result = "result of funcAsync";
    return (result);
}

●●●●.addEventListner('○○○○', () => {
    const result = funcSync();
    console.log("function finished.", result);
});

Promise を返す関数を作成する

JavaScript に用意(2015 年)された Promise を使います。

function funcAsync() {
    return new Promise((resolve, reject) => {
        時間の掛かる処理
        const result = "result of funcAsync";
        resolve(result);
    });
}

●●●●.addEventListner('○○○○', () => {
    funcAsync()
    .then((result) => {
        console.log("function finished.", result);
    });
});

async を使って関数を作成する

JavaScript に用意(2017 年)された async を使います。

async function funcAsync() {
    時間の掛かる処理
    const result = "result of funcAsync";
    return (result);
}

(以下略)

return new Promise() ~ resolve()async で書換しています。呼出する側は同じです。

await を使って関数を呼出する

async と併せて用意された await を使います。

(前略)

●●●●.addEventListner('○○○○', async () => {
    const result = await funcAsync();
    console.log("function finished.", result);
});

関数呼出.then()async ~ await で書換しています。呼出される側は同じです。

await を使うとよかったこと

async ~ await を使うことで、コード量も減って見やすくなります。
関数の定義に async をつけるので、非同期で実行されることが分かります。

呼出する側は 関数呼出.then() でもいいのですが、await を使うといいことがありました。IDE などでブレークポイントを設定してステップ実行したときです。

    funcAsync()                                   ここにブレークポイントを設定
    .then((result) => {
        console.log("function finished.", result);
    });

    次の処理                                       ステップ実行するとここに来る
    const result = await funcAsync();             ここにブレークポイントを設定
    console.log("function finished.", result);    ステップ実行するとここに来る

    次の処理

呼出して終了までステップ実行したいとき、then() の内にブレークポイントを設定しないといけなかったのが、必要なくなりました。

呼出された関数で問題あったとき

呼出された関数で処理していて問題あったとき、それを呼出した側に返すのはどうしたらいいでしょうか。

Promise で reject() する

そのために Promisereject() が用意されています。

function funcResolve() {
    return new Promise((resolve, reject) => {
        var result = "result funcResolve";
        resolve(result);    // 処理に問題なし
    });
}

function funcReject() {
    return new Promise((resolve, reject) => {
        var result = "result funcReject";
        reject("error funcReject");    // 処理に問題あり
    });
}
    funcResolve()
    .then((result) => {
        console.log("function finished.", result);
    });

    funcReject()
    .then((result) => {
        console.log("function finished.", result);
    });

これを実行すると

function finished. result funcResolve
Uncaught UnhandledPromiseRejection UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "result funcReject".

resolve() が実行されたときは then() で、reject() が実行されたときは catch() で受取するようになっています。

    (前略)
    
    funcReject()
    .then((result) => {
        console.log("function succeeded.", result);
    })
    .catch((error) => {
        console.log("function failed.", error)
    });

実行すると

function failed. error funcReject

async 関数で throw する

上記の処理を async で書くときはどうなるでしょうか。

async function funcResolve() {
    const result = "result funcResolve";
    return result;
}

async function funcReject() {
    const result = "result funcReject";
    throw new Error("error funcReject");    // 処理に問題あり
}

(以下略)

呼出される関数で throw するようです。このときは Error オブジェクトを生成して返すのが定番のようです。↑

    (前略)
    
    funcReject()
    .then((result) => {
        console.log("function succeeded.", result);
    })
    .catch((err) => {
        console.log("function failed.", err.message)
    });

実行すると

function failed. error funcReject

呼出する側で catch() で受取できています。Error オブジェクトを throw しているので、それを受取しています。

try ~ catch で受取する

関数呼出.then() でなく await を使うとどうなるでしょうか。

(前略)

    let result = await funcResolve();
    console.log("function finished.", result);

    result = await funcReject();
    console.log("function finished.", result);

これを実行すると

function finished. result funcResolve
Uncaught Error Error: error funcReject

throw したときは try ~ catch しないといけませんね。

    (前略)

    try {
        let result = await funcReject();
        console.log("function succeeded.", result);
    }
    catch(err) {
        console.log("function failed.", err.message);
    }

これを実行すると

function failed. error funcReject

調べていくと、await ~ catch の書き方もあるようです。

    (前略)

    let result = await funcReject()
    .catch((err) => {
        console.log("function failed.", err.message);
        return "function failed";
    });
    console.log("function finished.", result);

実行すると

function failed. error funcReject
function finished. function failed

ところで、throw して then() ~ catch() で受取できましたが、reject() して try ~ catch できるでしょうか。

function funcReject() {
    return new Promise((resolve, reject) => {
        var result = "result funcReject";
        reject("error funcReject");    // 処理に問題あり
    });
}
    (前略)

    try {
        let result = await funcReject();
        console.log("function succeeded.", result);
    }
    catch(error) {
        console.log("function failed.", error);
    }

実行すると

function failed. error funcReject

呼出される関数が Promise で実装されているか async で実装されているか気にしないで、呼出する側も then() ~ catch() でも try ~ catch でも実装できますね。

2
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?