4
4

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.

【JavaScript】Promise実践パターン

Posted at

はじめに

JavaScriptTypeScriptにちゃんと触るようになって約半年。その過程で扱った非同期処理のパターンをまとめていきたいと思います。
中級者向けの内容だと思いますので偉そうなこと言ってお前は中級者なのか、基礎から固めたいという方は手前味噌ですがこちらをどうぞ。

こんな人向け

  • Promiseawait辺りの仕組みがなんとなく分かってきた
  • 非同期処理のネストが深くなってしまっている
  • firestoreaxiosを使い始めた・使いたい

本編

1. 非同期in非同期

これはホントよく出会うパターン。非同期処理の結果を別の非同期関数に渡してその結果を・・・(ry
非同期が入れ子になっていってネスト地獄になってしまいます。

微妙.js
// 非同期関数1
const p1 = async() => {
  return 1;
};

// 非同期関数2
const p2 = async(index) => {
  const l = ['A', 'B'];
  return l[index];
};

// 非同期関数1で取得した値で非同期関数2を実行する
const main = async() => {
  p1().then(index => {
    p2(index).then(result => {
      console.log('result', result);
    })
  });
};

main();

// "result" "B"

頭のいい人は大丈夫でも、自分みたいなスペックだとだんだん処理が追えなくなってきます。
ので、以下のようにしてみます。

マシ.js
// 非同期関数1
const p1 = async() => {
  return 1;
};

// 非同期関数2
const p2 = async(index) => {
  const l = ['A', 'B'];
  return l[index];
};

// 非同期関数1で取得した値で非同期関数2を実行する
const main = async() => {
  const index = await p1();
  const result = await p2(index);
  console.log('result', result);
};

main();

あ^〜、うざったいネストが消えてスッキリしました。
脳死で書いているとついついネストにしてしまいがちですが、意識して平らになるようにするといいと思います。

2. エラー値も同時に取得したいパターン

そうは言ってもエラーの値も返したい時どうすんだよ。

微妙.js
// 非同期関数1
const p1 = async() => {
  return 2;
};

// 非同期関数2
const p2 = async(index) => {
  const l = ['A', 'B'];
  const r = l[index];
  if (!r) {
    throw('Error: index out of range.');
  }
  return r;
};

// 非同期関数1で取得した値で非同期関数2を実行する
const main = async() => {
  const index = await p1();
  let result;
  await p2(index)
    .then(r => {
      result = r;
    })
    .catch(e => {
      result = e;
    });
  console.log('result', result);
};

main();

// "result" "Error: index out of range."

でたよlet。コイツの悪いところは、どこで値が変わるかわかりづらいこと。できれば使いたくない。どら○も〜ん、なんか出してよ〜。

マシ.js
// 非同期関数1
const p1 = async() => {
  return 2;
};

// 非同期関数2
const p2 = async(index) => {
  const l = ['A', 'B'];
  const r = l[index];
  if (!r) {
    throw('Error: index out of range.');
  }
  return r;
};

// 非同期関数1で取得した値で非同期関数2を実行する
const main = async() => {
  const index = await p1();
  const result = await p2(index).catch(e => e); // return e; と同義
  console.log('result', result);
};

main();

// "result" "Error: index out of range."

の○太くん、そういう時はね、catch()の中でreturnしちゃえばいいのさ。
これでletとはサヨナラバイバイです。

3. then()の中で処理した結果を返したい

// 非同期関数1
const p1 = async() => {
  return 1;
};

// 非同期関数2
const p2 = async(index) => {
  const l = ['A', 'B'];
  const r = l[index];
  if (!r) {
    throw('Error: index out of range.');
  }
  return r;
};

// 非同期関数1で取得した値で非同期関数2を実行する
const main = async() => {
  const index = await p1();
  const isB = await p2(index).then(r => r === 'B');
  console.log('result', isB);
};

main();

// "result" true

非同期処理の結果からなんらかの処理をした値が実際に欲しい値、というパターンでは有用です。
これならぶっちゃけ処理の中身を見なくても「結果がBかどうか」が返ってくると分かるので個人的に好きです。

4. rejectバケツリレー

非同期処理の結果を処理して返す関数から、最初の非同期処理のエラーをそのまま引き渡したい時があります。

// 非同期関数1
const p1 = async(index) => {
  const l = ['A', 'B'];
  const r = l[index];
  if (!r) {
    throw('Error: index out of range.'); // 非同期関数1 -> 非同期関数2
  }
  return r;
};

// 非同期関数2
const p2 = async() => {
  const result = await p1(2).catch(e => {
    throw(e); // 非同期関数1 -> 非同期関数2 -> main
  });
  const res = 'result is ' + result;
  return res;
};

// 非同期関数1で取得したエラーを非同期関数2から返す
const main = async() => {
  const result = await p2().catch(e => {
    console.log('Error', e);
  });
  console.log('result', result);
};

main();

// "Error" "Error: index out of range."
// "result" undefined

これも単純な話でthrow()を使ってエラーをそのまま渡しちゃえばOKです。

5. 非同期処理の結果で非同期処理ループ

これもめっちゃよく見るパターン。.forEach()内ではawaitが使えないのでfor-ofを使って・・・こんな感じ?

微妙.js
// 非同期関数1
const p1 = async(index) => {
  return [0, 1];
};

// 非同期関数2
const p2 = async(index) => {
  const l = ['A', 'B'];
  const r = l[index];
  if (!r) {
    throw('Error: index out of range.');
  }
  return r;
};

// 非同期関数1で取得した値から非同期ループ
const main = async() => {
  const indexList = await p1();
  const results = [];
  for (const index of indexList) {
    results.push(await p2(index));
  }
  console.log("results", results);
};

main();

// "results" ["A", "B"]

やりたいことはできていますけれど、「非同期処理」の強みをお忘れではありませんこと?そう、「並列化」ですわ!

マシですわ.js
// 非同期関数1
const p1 = async(index) => {
  return [0, 1];
};

// 非同期関数2
const p2 = async(index) => {
  const l = ['A', 'B'];
  const r = l[index];
  if (!r) {
    throw('Error: index out of range.');
  }
  return r;
};

// 非同期関数1で取得した値から非同期ループ
const main = async() => {
  const indexList = await p1();
  const tasks = indexList.map(index => p2(index));
  const results = await Promise.all(tasks);
  console.log('results', results);
};

main();

// "results" ["A", "B"]

Promise.all()は渡した非同期処理を並列で実行して、全てが「満足」状態(resolve)になることで「満足」状態になる非同期関数です。

6. エラーを想定した並列処理

上でPromise.all()を使いましたが、コイツにも弱点があります。それは、一つでも「拒絶」状態(reject)になると「拒絶」状態になってしまうことです。まずは上の例で意図的にエラーを起こしてみます。

タヒんだんじゃないの〜☆.js
// 非同期関数1
const p1 = async(index) => {
  return [0, 1, 2];
};

// 非同期関数2
const p2 = async(index) => {
  const l = ['A', 'B'];
  const r = l[index];
  if (!r) {
    throw('Error: index out of range.');
  }
  return r;
};

// 非同期関数1で取得した値から非同期ループ
const main = async() => {
  const indexList = await p1();
  const tasks = indexList.map(index => p2(index));
  console.log('開始');
  const results = await Promise.all(tasks);
  console.log('終了');
  console.log(results);
};

main();

// "開始"

Promise.all()、完全に沈黙。しかもawaitで待っているので次の処理にすら進めません。
こんなことになりかねないので、扱いには注意しましょう。

エラーが想定される場合はPromise.allSettled()を使います。

// 非同期関数1
const p1 = async(index) => {
  return [0, 1, 2];
};

// 非同期関数2
const p2 = async(index) => {
  const l = ['A', 'B'];
  const r = l[index];
  if (!r) {
    throw('Error: index out of range.');
  }
  return r;
};

// 非同期関数1で取得した値から非同期ループ
const main = async() => {
  const indexList = await p1();
  const tasks = indexList.map(index => p2(index));
  const results = await Promise.allSettled(tasks);
  console.log(results);
};

main();

/*
[
  Object {
    status: "fulfilled",
    value: "A"
  },
  Object {
    status: "fulfilled",
    value: "B"
  },
  Object {
    status: "rejected",
    reason: "Error: index out of range."
  },
]
*/

Promise.allSettled()はこんな感じで結果を返します。statusは「満足」or「拒絶」、valueは「満足」結果、reasonはもちろん「拒絶」理由、といった内訳です。
まぁ、「成功したやつだけ欲しい」なら以下のようにすればいいでしょうか。

成功したやつだけ欲しい.js
const results = (await Promise.allSettled(tasks))
  .filter(r => r.status === 'fulfilled')
  .map(r => r.value);
console.log(results);

7. 一つでも成功したら満足

このパターンにはあまり出会ったことがありませんが、流れで紹介します。

// 非同期関数1
const p1 = async(index) => {
  return [2, 1, 0]; // reject, resolve, resolve
};

// 非同期関数2
const p2 = async(index) => {
  const l = ['A', 'B'];
  const r = l[index];
  if (!r) {
    throw('Error: index out of range.');
  }
  return r;
};

// 非同期関数1で取得した値から非同期ループ
const main = async() => {
  const indexList = await p1();
  const tasks = indexList.map(index => p2(index));
  const result = await Promise.any(tasks);
  console.log('result', result);
};

main();

// "result" "B"

Promise.any()は一度でも「満足」状態になったら、その段階で「満足」状態になります。
上の例だと、2つ目で成功してBが返ってきていますね。

まとめ

  • なるべく「平ら」なコードにしましょう
  • constこそ絶対神(異論は認める)
  • 非同期関数の「拒絶」はとりまthrow()
  • then()catch()内からのreturnを有効活用しましょう
  • 非同期と並列化は使いよう

最後まで読んでいただきありがとうございました!

4
4
0

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
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?