async/await周りで苦しんだ箇所の例と対策

async/awaitで苦しんだので備忘録です。
2022.08.19

どうも。CX事業本部Delivery部のえーたん(@eetann092)です。

今まで雰囲気でJavaScript(TypeScript)のasync/await、Promiseを使っていて最近苦しんだため、ハマったところの例と対策を備忘録として残しておきます。

await付け忘れ

やらかした例

まず、awaitを付け忘れた例です。

import { setTimeout } from "timers/promises";

const unit = 1000;

async function logSleepLog(msg: string) {
  console.log(msg);
  await setTimeout(1.5 * unit);
  console.log(msg);
}

(async () => {
 logSleepLog("hoo");
 console.log("finish");
 })();
hoo
finish
hoo

書き直した例

次に、awaitを付けた例です。

import { setTimeout } from "timers/promises";

const unit = 1000;

async function logSleepLog(msg: string) {
  console.log(msg);
  await setTimeout(1.5 * unit);
  console.log(msg);
}

(async () => {
  await logSleepLog("hoo");
  console.log("finish");
})();
hoo
hoo
finish

対策

@typescript-eslint/eslint-pluginには、awaitの書き忘れを防ぐためのルールno-floating-promisesがあります。

今回はIIFE(Immediately Invoked Function Expression 即時実行関数式)を使っているため、IIFEでは怒られないようにignoreIIFEtrueにします。

eslintの設定の例は以下です。

.eslintrc.js

  rules: {
    "@typescript-eslint/no-floating-promises": [
      "warn",
      {
        ignoreIIFE: true
      }
    ],

awaitを付け忘れると、以下のようにエラーで怒ってくれます。

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the void operator. [@typescript-eslint/no-floating-promises]

forEachの中でasync/await

やらかした例

まず、forEachの中でasync/awaitを書いた例です。

import { setTimeout } from "timers/promises";

const unit = 1000;

async function logSleepLog(msg: string) {
  console.log(msg);
  await setTimeout(1.5 * unit);
  console.log(msg);
}

(async () => {
  const foo = [1, 2];
  foo.forEach(async (f) => {
    await logSleepLog(`${f}`);
  });
  console.log("finish");
})();
1
2
finish
1
2

書き直した例

次に、for ofを使って書き直した例です。以下の記事を参考にしました。

import { setTimeout } from "timers/promises";

const unit = 1000;

async function logSleepLog(msg: string) {
  console.log(msg);
  await setTimeout(1.5 * unit);
  console.log(msg);
}

(async () => {
  const foo = [1, 2];
  for (const f of foo) {
    await logSleepLog(`${f}`);
  }
  console.log("finish");
})();
1
1
2
2
finish

対策

@typescript-eslint/eslint-pluginには、async/awaitを使うべきではない書き方を怒ってくれるルールno-misused-promisesがあります。

eslintの設定の例は以下です。

.eslintrc.js

  rules: {
    "@typescript-eslint/no-floating-promises": ["warn", { ignoreIIFE: true }],
    "@typescript-eslint/no-misused-promises": "warn",

async/awaitを使うべきではない書き方では、以下のようにエラーで怒ってくれます。

Promise returned in function argument where a void return was expected. [@typescript-eslint/no-misused-promises]

Promise.allはすべてのエラーをcatchするわけではない

勘違いした例

Promise.allはすべてのエラーをcatchするわけではないようです。

以下、Promise.allと4つのエラーを起こす例です。

import { setTimeout } from "timers/promises";

const unit = 1000;

async function hoge(num:number, msg:string) {
  const a = 'hoge';
  await setTimeout(num * unit);
  if (a===msg) {
    console.log(`正常 ${num}:${msg}`)
  } else {
    console.log(`エラーが起きた ${num}:${msg}`)
    throw `throwのメッセージ ${num}:${msg}`
  }
}

(async () => {
  const promises = [];
  promises.push(hoge(1, 'hoge')); // 1秒後に 正常
  promises.push(hoge(2, 'foo'));  // 2秒後に エラー
  promises.push(hoge(5, 'bar'));  // 5秒後に エラー
  promises.push(hoge(4, 'bar'));  // 4秒後に エラー
  promises.push(hoge(3, 'bar'));  // 3秒後に エラー
  await Promise.all(promises).catch(e => console.log(e))
  console.log("finish");
})();

以下が出力です。catchされたエラーは最初の2のみです。また、Promise.allでは、catchが終わった時点で次の行("finish"の表示)の処理に移るようです。

正常 1:hoge
エラーが起きた 2:foo
throwのメッセージ 2:foo
finish
エラーが起きた 3:bar
エラーが起きた 4:bar
エラーが起きた 5:bar

書き直した例

Promise.allSettledを使えば、すべてのプロミスが終わってから結果を返してくれます。

ちなみに "settled" は「確立した、定まった」などの意味を持つ単語のようです(参考:英語「settled」の意味・読み方・表現 | Weblio英和辞書)。

以下が、Promise.allSettledを使って書き直した例です。
成功した場合、受け取った結果のstatus"fulfilled"になり、valueで値を取得できます。
失敗した場合、受け取った結果のstatus"rejected"になり、reasonで理由を取得できます。

import { setTimeout } from "timers/promises";

const unit = 1000;

async function hoge(num: number, msg: string) {
  const a = "hoge";
  await setTimeout(num * unit);
  if (a === msg) {
    console.log(`正常 ${num}:${msg}`);
  } else {
    console.log(`エラーが起きた ${num}:${msg}`);
    throw `throwのメッセージ ${num}:${msg}`;
  }
}

(async () => {
  const promises = [];
  promises.push(hoge(1, "hoge")); // 1秒後に 正常
  promises.push(hoge(2, "foo")); // 2秒後に エラー
  promises.push(hoge(5, "bar")); // 5秒後に エラー
  promises.push(hoge(4, "bar")); // 4秒後に エラー
  promises.push(hoge(3, "bar")); // 3秒後に エラー
  await Promise.allSettled(promises).then((results) =>
    results.map((result) => {
      if (result.status === "rejected") {
        console.log(result.reason);
      }
    })
  );
  console.log("finish");
})();

以下が出力です。すべての実行が終わってからエラーの内容を吐き出しています。

正常 1:hoge
エラーが起きた 2:foo
エラーが起きた 3:bar
エラーが起きた 4:bar
エラーが起きた 5:bar
throwのメッセージ 2:foo
throwのメッセージ 5:bar
throwのメッセージ 4:bar
throwのメッセージ 3:bar
finish

参考:

リンク集

サンプルコードのGitHubのリンクは以下です。