2020/12/16 追記
冒頭の元記事のプログラムを読み間違えていて、真面目に読むといきなり仕様とは異なるプログラムを書いてしまっていますorz
その旨を指摘していただいたコメントがコメント欄にあるのですが、そちらの方がコードとしては面白いことになってるのでコメントまで合わせて読むのがおすすめです。
本文
この記事は、以下の記事を読んだ自分の感想です。割と反対意見が多めです。
あと、元々 C# 畑の人間なので、この記事で TypeScript を使ってますが、もしかしたら冗長な書き方や、そもそも勘違いをしてしまっている可能性があります。その場合はコメントなどで教えてください。
そもそも、async/await は Promise を置き換えるものではなくて、どちらかというと then/catch/finally のメソッドチェーンなどを置き換えるものですし、Promise という文字を書いたら負けという類のものではないです。
JavaScript では、たまたま戻り値とかの型を書かなくてもいいので async/await をシンプルに使っただけだと Promise という文字列が出ないので Promise を置き換えるように見えるかもしれませんが、そういう話しが今回の主題ではないと思ってます。
そのため、この記事では「Promise を使ったコード」を then
や catch
や finally
などのメソッドチェーンやコールバックを使って非同期処理を書いているコードという認識で書いています。
要約
元記事で 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 を使って全部の非同期処理が終わる(成功・失敗問わず)まで待つとかもできるみたいです。
cache→catchですかね。あとES2020なら、Promise.allSettled()を使って、try…catchを使わずに書けますね。(一個一個成功したかを判定しなきゃいけないですが)
— Masaki Suzuki@フリーランスクラウドエンジニア (@makky12) December 13, 2020
Promise を使わないと書けない例
個人的には Promise じゃないと書けない例としては constructor の中や index.ts のようなエントリーポイントの中に直接書くようなときは Promise じゃないと書けません。(コンストラクター内で非同期処理呼ぶのどうよ?というのは置いといて)
その場合でも、メソッドに切り出してしまえばメソッド内では await 使えるので最終的には async/await にします。
async function main() {
// ここは await 使える
}
main();
個人的な感想
.then
や .catch
や .finally
を使った方が素直に書けるケースであれば、使えばいいと思いますが個人的には非常に限られる(自分的には思いつかない…)ので、async/await で置き換えれるところは積極的に async/await を使うといいと思っています。
その上で、元記事で性能が悪いのでダメと言われていたケースのように並行して実行したほうが効率が良いものを並行して実行していないといったケースは Promise.all
、Promise.any
、Promise.race
などを使って書く方法を覚えるほうが良いと思っています。
fire and forget で例外握り潰しを明示したいときとかは Promise のほうが直感的かも?というのを思ったので最後に書いておきます。
downloadUrl(new URL('https://example.com')).catch(function ignore() {});
Can I fire and forget a promise in nodejs (ES7)?
以上です。