87
56

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

async/await は Promise を置き換えることは出来ると思う

Last updated at Posted at 2020-12-13

2020/12/16 追記

冒頭の元記事のプログラムを読み間違えていて、真面目に読むといきなり仕様とは異なるプログラムを書いてしまっていますorz
その旨を指摘していただいたコメントがコメント欄にあるのですが、そちらの方がコードとしては面白いことになってるのでコメントまで合わせて読むのがおすすめです。

本文

この記事は、以下の記事を読んだ自分の感想です。割と反対意見が多めです。

async/await は Promise を置き換えない

あと、元々 C# 畑の人間なので、この記事で TypeScript を使ってますが、もしかしたら冗長な書き方や、そもそも勘違いをしてしまっている可能性があります。その場合はコメントなどで教えてください。

そもそも、async/await は Promise を置き換えるものではなくて、どちらかというと then/catch/finally のメソッドチェーンなどを置き換えるものですし、Promise という文字を書いたら負けという類のものではないです。
JavaScript では、たまたま戻り値とかの型を書かなくてもいいので async/await をシンプルに使っただけだと Promise という文字列が出ないので Promise を置き換えるように見えるかもしれませんが、そういう話しが今回の主題ではないと思ってます。

そのため、この記事では「Promise を使ったコード」を thencatchfinally などのメソッドチェーンやコールバックを使って非同期処理を書いているコードという認識で書いています。

要約

元記事で async/await で書けないパターンと紹介されてるやつが普通に async/await で書ける。もちろん async/await だと書きにくいパターンはあると思います。

本文

該当記事では、async/await では 書けない 処理とされている fan-in/fan-out の例として以下のようなコードが紹介されています。

const fetchProfileImageUrl = (username: string): Promise<URL> => { ... };
const downloadUrl = (url: URL): Promise<ArrayBuffer> => { ... };
const cacheUrl = (username: string, url: URL): Promise<void> => {...};

const imageUrl = fetchProfileImageUrl('okapies');
const image = imageUrl.then(url => downloadUrl(url));
imageUrl.then(url => cacheUrl('okapies', url));

ついでに、これを async/await で書くことで性能が劣化してしまうという指摘されているコードはこれです。

const imageUrl = await fetchProfileImageUrl('okapies');
const image = await downloadUrl(imageUrl);
await cacheUrl('okapies', imageUrl);

そもそもこのコード例は仕様が違います。元の Promise を使った方のコードは fetchProfileImageUrl の戻り値に対して downloadUrl と cacheUrl を並列で呼び出すです。async/await の方は fetchProfileImageUrl を呼び出したら downloadUrl メソッドを呼び出して完了を待ってから cacheUrl を呼び出すです。

async/await を使って fetchProfileImageUrl の処理が終わったあと downloadUrl と cacheUrlを並行して呼び出すならこんな感じです。

const imageUrl = await fetchProfileImageUrl('okapies');
const result = await Promise.all([
    downloadUrl(imageUrl),
    cacheUrl('okapies', imageUrl),
]);

ただ、これでも元のコードとはちょっと違って downloadUrl, cacheUrl がどちらかが失敗すると例外が飛んでしまう感じになっています。元の Promise を使ったコードのメソッドはサンプルなので例外処理は入れてないのでしょうが、きっと .catch や場合によっては .finally を追加して堅牢な感じに書いていくのだと思います。then や cache や finally が続いて後続処理があるなら async/await の場合はいつもやってるメソッドに切り出してあげるのが素直だと思います。

const downloadUrlAndSomething = async (imageUrl: URL) => {
    try {
        const image = await downloadUrl(imageUrl);
        // downloadUrl 正常終了時の続きの処理
        // もちろん、ここでも await 使える
    } catch {
        // 何かエラー処理
        // もちろん、ここでも await 使える
    } finally {
        // 何か後始末したければ
        // もちろん、ここでも await 使える
    }
};
    
const cacheUrlAndSomething = async (username: string, imageUrl: URL) => {
    try {
        await cacheUrl('okapies', imageUrl);
        // cacheUrl 正常終了時の続きの処理
        // もちろん、ここでも await 使える
    } catch {
        // 何かエラー処理
        // もちろん、ここでも await 使える
    } finally {
        // 何か後始末したければ
        // もちろん、ここでも await 使える
    }
};

const imageUrl = await fetchProfileImageUrl('okapies');
await Promise.all([
    downloadUrlAndSomething(imageUrl),
    cacheUrlAndSomething('okapies', imageUrl),
]);

他にも元記事の Promise.any を使ったコード例も Promise.any を await すれば OK です。

追記

Twiter で教えてもらったのですが ES2020 なら Promise.allSettled を使って全部の非同期処理が終わる(成功・失敗問わず)まで待つとかもできるみたいです。

Promise を使わないと書けない例

個人的には Promise じゃないと書けない例としては constructor の中や index.ts のようなエントリーポイントの中に直接書くようなときは Promise じゃないと書けません。(コンストラクター内で非同期処理呼ぶのどうよ?というのは置いといて)

その場合でも、メソッドに切り出してしまえばメソッド内では await 使えるので最終的には async/await にします。

index.ts
async function main() {
  // ここは await 使える
}

main();

個人的な感想

.then.catch.finally を使った方が素直に書けるケースであれば、使えばいいと思いますが個人的には非常に限られる(自分的には思いつかない…)ので、async/await で置き換えれるところは積極的に async/await を使うといいと思っています。

その上で、元記事で性能が悪いのでダメと言われていたケースのように並行して実行したほうが効率が良いものを並行して実行していないといったケースは Promise.allPromise.anyPromise.race などを使って書く方法を覚えるほうが良いと思っています。

fire and forget で例外握り潰しを明示したいときとかは Promise のほうが直感的かも?というのを思ったので最後に書いておきます。

downloadUrl(new URL('https://example.com')).catch(function ignore() {});

Can I fire and forget a promise in nodejs (ES7)?

以上です。

87
56
7

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
87
56

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?