質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.80%

asyncブロック内でmap関数内のawaitが使えない。

受付中

回答 2

投稿

  • 評価
  • クリップ 0
  • VIEW 394

yuki_90453

score 186

概要

下記のようなasync関数内で、配列にmap関数で処理を行います。

export const handler=async(event: APIGatewayEvent, context:Context, callback:Callback)=>{
    const array_1 = ["aaa","bb","cccc"]
    array_1.map((str:any)=>{
        await updateProduct(str)
    })
}
export const updateProduct =async(str)=>{
    const shopify = new Shopify2({
       (省略)
    })
    await shopify.product.update(str)
    .then((res:any)=>{
        res
    })
    .catch((res:any)=>{
        res
    })
}


上記のawait式が「非同期関数内で使用出来ます」とコンパイルエラーになってしまいます。
handler関数でasync宣言しているのでmap内まで非同期が有効であると思うのでこのようなエラーになってしまい、困っております。

怪しい点などアドバイス頂けないでしょうか。
宜しくお願いします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

+1

handler関数でasync宣言しているのでmap内まで非同期が有効であると思うので

残念ながら、awaitasync関数内に直接書くことしかできません。

「コールバックを要さないfor系の構文で回しながらawaitを行う(1個ずつ実行される)」もしくは「全部をmapしてPromiseの配列に変換した上で、await Promise.all(配列)とする(全部並列実行)」などの手段が必要となります。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+1

問題点はシンプルで、async関数直下でしかawait構文は使えません。
入れ子OKにするとでかいasyncメイン関数内ならどこでも使い放題みたいになってコードが崩壊しますからね。
なのでArray.prototype.map(以下map)内でawaitを使いたければmapに引数もasync関数にしてやる必要があります。

しかしmapでasync関数を突っ込んでもPromiseの配列になるだけです。
なので単純にasync関数を突っ込むだけでは上手く行きません。
Promise.allを使いましょう。

私はTypeScriptに関してはあまり詳しくないのでJavaScriptで書きますが、
解説付きコードで表現するとこんな感じです。

const handler = async (event, context, callback) => {
  const array_1 = ["aaa","bb","cccc"]

  // mapのコールバック関数もasync関数でくくってしまう。
  const array_2 = array_1.map(async str =>
    await updateProduct(str)
  )

  // Array.prototype.map内でasync関数作ると
  // Promiseインスタンスの配列が払い出される
  console.log(array_2) // [Promise, Promise, ...]

  // もしupdateProductがPromiseを返すならこれでOK
  // const array_2 = array_1.map(updateProduct)

  // Promise.allにPromiseインスタンスの配列を突っ込めば
  // 全ての実行を待って配列を取り出す一つのPromiseインスタンスになる
  // そうすればawaitで待ちつつ配列形式で値を取り出せる
  const array_result = await Promise.all(array_2)
}

ですが、そもそもupdateProductという名称から目的を推測するに、
非同期処理を伴う戻り値Promiseインスタンスですよね?
もし違うならawaitで待ちたいとかお前は何を言っているんだ?みたいな状態になってしまいますね。

上記を踏まえてシンプルに書くと、
最終形はこんな感じになっていると考えられます。

const handler = async (event, context, callback) => {
  const array_1 = ["aaa","bb","cccc"]
  const result = await Promise.all(
    array_1.map(updateProduct)
  )
}

ただ外部のAPIと通信するのでPromise.allでは並列で1度にAPIを叩いてしまう処理になってしまいlimitを超えてしまう為、使用を控えております。

mapというのは値を関数に従って変形させた結果を取り出すという目的に使われるものなので、
高度な非同期処理を内包したものを作るには不向きです。
なので、基本的にはfor文でお行儀よく1個ずつ処理を走らせる方向で考えるのが自然だと思います。

それでもリスト操作でこなすなら
100個のリクエストというボールが入ったプールがあったとして、
バケツで5個ずつ組み上げる作戦があります。

Lodashのchunk関数が丁度それに使えます。
実装をコードになおすと下記のような感じ。

const range = (a, b) => Array(b - a + 1).fill(0).map((_, i) => i + a);
console.log(range(1, 10)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

const chunk = (arr, size) => arr.reduce((result, it, i) => {
  if (!result[Math.floor(i / size)]) result[Math.floor(i / size)] = [];
  result[Math.floor(i / size)].push(it);
  return result;
}, []);
console.log(JSON.stringify(chunk(range(1, 21), 5), null, 2));
[
  [
    1,
    2,
    3,
    4,
    5
  ],
  [
    6,
    7,
    8,
    9,
    10
  ],
  [
    11,
    12,
    13,
    14,
    15
  ],
  [
    16,
    17,
    18,
    19,
    20
  ],
  [
    21
  ]
]

これを実際に作るならreduceを使いながらthenを次々に転がすような実装になります。

const chunk = (arr, size) => arr.reduce((result, it, i) => {
  if (!result[Math.floor(i / size)]) result[Math.floor(i / size)] = []
  result[Math.floor(i / size)].push(it)
  return result
}, [])

const handler = async (event, context, callback) => {
  const array_1 = ["aaa","bb","cccc"]
  const limit = 5
  await chunk(array_1, limit).reduce((promise, arr) => {
    promise.then(() => Promise.all(
      arr.map(updateProduct)
    ))
    return promise
  }, Promise.resolve(1))
}

まぁ、chunkのアイデアまでは良いと思いますが、
reduceは明らかにES5まで退行しているので、
素直にfor...ofを使った方が100倍見栄えは良いでしょう。

const chunk = (arr, size) => arr.reduce((result, it, i) => {
  if (!result[Math.floor(i / size)]) result[Math.floor(i / size)] = []
  result[Math.floor(i / size)].push(it)
  return result
}, [])

const handler = async (event, context, callback) => {
  const array_1 = ["aaa","bb","cccc"]
  const limit = 5
  for (const arr of chunk(array_1, limit)) {
    await Promise.all(
      arr.map(updateProduct)
    )
  }
}

最速で行きたいなら
stateを使って状態管理した方が良いでしょうね。
状態管理に依存するので使うとするならforEach1個だけで頑張るのが無難ですが、
だったらやはりfor...ofでよくね?と感じます。

まぁ、Babelはasync関数とfor...ofの同時利用を解決出来るか怪しいので、
forEachで書けそうなら頑張っても良いと思いますが。

const waitFor = (ms) => new Promise(resolve) =>
const waitUpdate = async (state, limit) => {
  while (true) {
    if (state.map(it => it.loading).length < limit) break
    await waitFor(1000) // リミット件数実行中なら1秒程度待つ
  }
}

const handler = async (event, context, callback) => {
  const array_1 = ["aaa","bb","cccc"]
  const limit = 5
  const states = array_1.map(str => ({str, loading: false}))
  for (const state of states) {
    await waitUpdate(states, limit)
    state.loading = true
    updateProduct(state.str).then(() => state.loading = false)
  }
}

大体こんな感じがゴールになるでしょう。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/03/05 15:03

    詳細の回答ありがとうございます。
    いくつか質問お願い致します。
    >しかしmapでasync関数を突っ込んでもPromiseの配列になるだけです。
    以前から気になっていたのですがPromiseを用いた処理で出力されたPromise〜とは一般的なstringやnumberとは型が出力されるのでしょうか?

    >ですが、そもそもupdateProductという名称から目的を推測するに、
    >非同期処理を伴う戻り値Promiseインスタンスですよね?
    仰る通りです。ただ外部のAPIと通信するのでPromise.allでは並列で1度にAPIを叩いてしまう処理になってしまいlimitを超えてしまう為、使用を控えております。

    やはりmapなどで処理せずブロック内でawaitを使えるforで書いた方が、良いのでしょうか。

    宜しくお願いします

    キャンセル

  • 2020/03/05 21:07

    前者はPromiseインスタンスはPromiseオブジェクトをnewして作られたインスタンスです。
    TypeScriptでPromiseインスタンスからthenメソッドを叩いて得られる引数の型の話なら、
    制御しにくいとは思いますが、私はTypeScriptを触ってないのでその辺は別途勉強してください。

    後者に関しては完全に後出しですが、追記しておきます。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.80%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る