🤞

【自戒】Promiseを返す関数を反復処理するときの注意事項

2021/08/19に公開1

はじめに

※Promiseオブジェクトをちゃんと理解していなかった自分に対しての戒めを含みます。

事象について

表題の通りですが、 Promiseを返す処理を反復させ、その後に実行すべき処理がその反復処理の前に実行されてしまうという事象でした。
例を挙げると、下記のようなコードです。

const strs = ['test1', 'test2', 'test3']; 

const asyncFuncStr = (str) => {
  return new Promise((resolve, reject)=> {
    setTimeout(() => {
      console.log(str)
      resolve()
    }, 2000)
  })
}

const mapFunc = async() => {
  return strs.map((s) => asyncFuncStr(s))
}

mapFunc().then((res) => {
  console.log('finished')
});

2秒後にそれぞれの文字列をコンソールに出力する関数「asynFuncStr」=「Promiseを返す処理」です。

これを見て「だめやん」と思った方は、この先読む必要がないので離脱してください...

問題点について

上記においての問題点は、関数「mapFunc」です。
その中でもreturnしているものに問題があります。
実際にreturnされているものを見てみると、下記のような返り値になっています。

// [object Array] (3)
[// [object Promise] 
{},// [object Promise] 
{},// [object Promise] 
{}]

Promiseのオブジェクトが三つ返ってきていますね。
つまり、resolveされていない状態で値が返ってきてしまっています。意味ないですよね。

ではどうするか?

解決策について

今回の悪い点はmap内の反復処理の一つ一つを解決せずに返してしまっている、という点です。
そこで、下記のように修正します。

const strs = ['test1', 'test2', 'test3']; 

const asyncFuncStr = (str) => {
  return new Promise((resolve, reject)=> {
    setTimeout(() => {
      console.log(str)
      resolve()
    }, 2000)
  })
}

const mapFunc = async() => {
  return Promise.all(strs.map((s) => asyncFuncStr(s)))
}

mapFunc().then((res) => {
  console.log('finished')
});

変更点は一つだけで、mapの処理を「Promise.all」で囲うだけです。
先程の返り値も下記のように変わります。

// [object Array] (3)
// [undefined,undefined,undefined]

Promise.allについてはMDNを参照してください。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
つまり、Promiseで引数の処理をラップして、引数の処理のPromiseの解決までを待ってくれます。
めちゃくちゃ望ましい挙動ですよね。

まとめ

あんまりこのようなPromiseを返すような関数をラップして使うことがなかったので、つまずきました。
aync/awaitの恩恵に怠けず、ちゃんとPromiseオブジェクトから勉強すべきですね。

Discussion

nap5nap5

StaticなメソッドがPromiseからは提供されています。Promise.reject,Promise.resolveを使ってもいいかもと思いました。

import { test, expect } from "vitest";
import { ok, err, Ok, Err } from "neverthrow";

test("Promise.all", async () => {
  const inputData = [1, 2, 3, 4, 5];
  const doTask = (d: number) => {
    if (d % 2 === 0) return Promise.reject(new Error("偶数です", { cause: d }));
    return Promise.resolve(d);
  };
  const pList = inputData.map(doTask);
  try {
    const results = await Promise.all(pList);
  } catch (error) {
    expect(error).toStrictEqual(new Error("偶数です", { cause: 2 }))
  }
});

test("Promise.allSettled", async () => {
  const inputData = [1, 2, 3, 4, 5];
  const doTask = (d: number) => {
    if (d % 2 === 0) return Promise.reject(d);
    return Promise.resolve(d);
  };
  const pList = inputData.map(doTask);
  const results = await Promise.allSettled(pList);
  expect(results).toStrictEqual([
    { status: 'fulfilled', value: 1 },
    { status: 'rejected', reason: 2 },
    { status: 'fulfilled', value: 3 },
    { status: 'rejected', reason: 4 },
    { status: 'fulfilled', value: 5 }
  ])
});

test("Result Monado Keep Summary", async () => {
  const inputData = [1, 2, 3, 4, 5];
  const doTask = (d: number) => {
    if (d % 2 === 0) return err(new Error("偶数です", { cause: d }));
    return ok(d);
  };
  const pList = inputData.map(doTask);
  const results = await Promise.all(pList);
  const values = results.flatMap((d) => (d.isErr() ? [] : d));
  const errors = results.flatMap((d) => (d.isOk() ? [] : d));
  expect({ summary: { values, errors } }).toStrictEqual({
    summary: {
      values: [new Ok(1), new Ok(3), new Ok(5)],
      errors: [
        new Err(new Error("偶数です", { cause: 2 })),
        new Err(new Error("偶数です", { cause: 4 })),
      ],
    },
  });
});

demo code.

https://codesandbox.io/p/sandbox/twilight-glade-qh94mw?file=/src/index.test.ts:1,1