iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

Promiseの静的メソッドを調べてみた

初めに


こんにちは、今回で3回目のブログ更新となります。

株式会社iimonでエンジニアをしている林です。

普段は、弊社プロダクトの拡張アプリの開発をしています。

本日はPromiseの静的メソッドついて解説していきたいと思います。

promiseの知識がある事を前提に解説しておりますのでよろしくお願いいたします。

promiseの静的メソッド

promiseの静的メソッドは以下の6種類です。

  • Promise.all()
  • Promise.allSettled()
  • Promise.any()
  • Promise.race()
  • Promise.reject()
  • Promise.resolve()

以下のファイルを作成し、一つ一つ解説をしていきます。

ハンズオンで実際に見たい方は以下のコマンドを実行しブラウザで確認お願いします。

mkdir promise-static-tutorial && cd $_
touch {promiseStatic.html,promiseStatic.js}

[promiseStatic.html]

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>promise-static</title>
</head>
<body>
  <h2>promise-staticについて</h2>
  <script src="promiseStatic.js"></script>
</body>
</html>

[promiseStatic.js]

  • 以下の様なresolveとrejectを返すpromiseをそれぞれ作りstaticメソッドを解説して行きます。
const resolvePromise1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve(1)
  }, 1000)
})
const resolvePromise2 = new Promise((resolve) => {
  setTimeout(() => {
    resolve(2)
  }, 2000)
})
const resolvePromise3 = new Promise((resolve) => {
  setTimeout(() => {
    resolve(3)
  }, 3000)
})

const rejectPromise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(1)
  }, 1000)
})
const rejectPromise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(2)
  }, 2000)
})

Promise.all([resolvePromise1, resolvePromise2, resolvePromise3, 4, {value: 5 }]).then((value) => {
  console.log('PromiseAllValue:', value)
}).catch((error) => {
  console.log('PromiseAllError:', error)
})

Promise.allSettled([resolvePromise1, resolvePromise2, rejectPromise1, rejectPromise2, 4 , {value: 5 }]).then((value) => {
  console.log('PeomiseAllSettledValue:', value)
}).catch((error) => {
  console.log('PromiseAllSetteldError:', error)
})

Promise.race([resolvePromise1, rejectPromise2, rejectPromise2]).then((value) => {
  console.log('PeomiseRaceValue:', value)
}).catch((error) => {
  console.log('race error:',error)
})

Promise.any([rejectPromise1, rejectPromise2]).then((value) => {
  console.log('PeomiseAnyValue:', value)
}).catch((error) => {
  console.log('any error:')
  console.dir(error)
})

Promise.resolve('value').then((value) => {
  console.log('promise.resolve:', value)
})

Promise.reject(new Error('error')).then((value) => {
  console.log('promise.reject value:', value)
}).catch((error) => {
  console.log('promise.reject error:', error)
})

Promise.All

  • promise.allは第一引数としてリテラブルオブジェクトを取る
  • 引数に渡されたpromise全てがresolveされた後にallメソッドがresolveされ、thenメソッドの第一引数のコールバックが実行される。
  • thenの第一引数には渡された引数の順番でコールバック値が入っている。(resoleveの順番によって入れ替わる事はない)
  • rejectされたら、それが複数あったとしても最初にrejectされた値がcatchに入る。
  • promise以外の普通の値も入れられる(その場合、即時にresolveされる)

***リテラブルオブジェクト→要素を一つずつ順番に取り出せるオブジェクトのこと

Promise.all([resolvePromise1, resolvePromise2, resolvePromise3, 4, {value: 5 }]).then((value) => {
  console.log('PromiseAllValue:', value)
}).catch((error) => {
  console.log('PromiseAllError:', error)
})

Promise.allSettled

  • Promise.allとほぼ同じだが、渡されたpromiseがrejectされたとしても中断されずにresolveされてthenが実行される。
  • thenの返り値はオブジェクトで返されてstatusというプロパティを持っており、そこにpromiseの状態が入っている。
  • resolveした場合はvaluerejectした場合はreasonにpromiseのresultが入っている。

Promise の状態は以下のいずれかとなります。

  • 待機 (pending): 待機中
  • 履行 (fulfilled): 処理成功
  • 拒否 (rejected): 処理失敗
Promise.allSettled([resolvePromise1, resolvePromise2, rejectPromise1, rejectPromise2, 4 , {value: 5 }]).then((value) => {
  console.log('PeomiseAllSettledValue ', value)
}).catch((error) => {
  console.log('PromiseAllSetteldError', error)
})

Promise.race

  • 一番初めにresolveかrejectしたものだけを対応する
Promise.race([resolvePromise1, rejectPromise2, rejectPromise2]).then((value) => {
  console.log('PeomiseRaceValue:', value)
}).catch((error) => {
  console.log('race error:',error)
})

Promise.any

  • rejectは無視し、一番初めにresolveされたもののみ対応する
  • 全てのpromiseがrejectだった場合のみcatchが実行される
    • catchに入った場合はerrorsにそれぞれのpromiseのresultが配列で入る
      Promise.any([rejectPromise1, rejectPromise2]).then((value) => {
        console.log('PeomiseAnyValue:', value)
      }).catch((error) => {
        console.log('any error:')
        console.dir(error)
      })
    

Promise.resolve

  • new Promiseですぐにresolveを返す場合の書き換え。

      Promise.resolve('value');
    

    上記と下記は同じ意味なのでPromise.resolveを使った方が簡潔な書き方となります。

          new Promise((resolve) => resolve('value'))
    

個人的にはPromise.resolveのresultによってハンドリングした事しかないのですが、

こちらの静的メソッドを使う場合は例えば以下のようなパターンが考えられそうです。

  • APIからのデータ取得をシミュレートする

    • 仮に開発中に実際のAPIがまだ利用できない場合、ダミーデータをPromise.resolve() でラップしてシミュレートする。
      const getMockData = () => {
        const mockData = { id: 1, name: "Alice" };
        return Promise.resolve(mockData);
      }
    
  • 動的に入ってくる値が同期的値と非同期的値の場合にPromiseでラップする

    • 上記のPromise.allやallSettledでも説明したとおり、promiseには同期的値を渡した場合も非同期にラッピングされ、resolveしたらfulfilledとして正常に値を返すので同期的値か非同期的値か動的に入ってくる値が分からないものをハンドリングしたい場合に使用することが考えられそうです。
    • 例えばローカルストレージに値がなければリモートストレージにアクセスして値を取得する関数があった場合、ローカルストレージのアクセスは同期的ですが、リモートストレージのアクセスは非同期的です。こうした場合、両方の関数が Promise を返すように Promise.resolve() を使うことで統一出来ると思います。
    • ただ、例として無理矢理挙げましたが個人的にはアクセス先が違う場合は関数を分けるのがベストプラクティスだと思うので、アサインしたプロジェクトの無理矢理共通化しようとしているコードで、もしかしたら見かけるかもくらいな気がします。
      • それ以外にはPromise.resolveの使用方法が思い浮かばなかったので、「絶対にここはPromise.resolveを使った方が簡潔だ!!!!」っていう使い方があったら是非教えてほしいです。

Promise.reject

  • new Promiseですぐにrejectを返す場合の書き換え。

      Promise.reject(new Error('error'))
    

    上記と下記は同じ意味なのでPromise.resolveを使った方が簡潔な書き方となる。

      new Promise((resolve, reject) => reject(new Error('error')))
    

Promise.rejectもPromise.resolve()と同じような感じで一緒に使うことの方が多いと思われます。Promise.resolve()する条件に一致しない場合に早期rejectさせてエラーハンドリングする時に使うのかなって思います。

ハンズオン

今回はチェックボックスでチェックをつけた犬種の画像をapiを叩いて取得し、

レンダリングする機能を作ってpromiseのAll, allSettled, any, raceを学んでみましょう!

犬以外の名前を選択するとapiがエラーを返しますのでrejectされます。

まずはファイルを作成します。

touch {promiseStaticDogApi.html,promiseStaticDogApi.js}

コードは以下になります。

[promiseStaticDogApi.html]

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>promiseStatic</title>
</head>
<body>
  <h2>犬の名前を選んでください</h2>
  <input type="checkbox" id="dog1" value="affenpinscher">
  <label for="dog1">affenpinscher</label><br>
  <input type="checkbox" id="dog2" value="african" checked="true">
  <label for="dog2">african</label><br>
  <input type="checkbox" id="dog3" value="akita" checked="true">
  <label for="dog3">akita</label><br>
  <input type="checkbox" id="dog4" value="brabancon">
  <label for="dog4">brabancon</label><br>
  <input type="checkbox" id="dog5" value="pembroke">
  <label for="dog5">pembroke</label><br>
  <input type="checkbox" id="dog6" value="pomeranian">
  <label for="dog6">pomeranian</label><br>
  <input type="checkbox" id="ra-men" value="ra-men">
  <label for="ra-men">ラーメン</label><br>
  <input type="checkbox" id="tsukemen" value="tsukemen">
  <label for="tsukemen">つけ麺</label><br>
  <div id="buttons">
    <button id="AllSubmitBtn">promiseAll送信</button>
    <button id="AllSettledSubmitBtn">promiseAllSettled送信</button>
    <button id="RaceSubmitBtn">promiseRace送信</button>
    <button id="AnySubmitBtn">promiseAny送信</button>
  </div>
  
  <div id="images"></div>
  
  <script src="promiseStaticDogApi.js"></script>
</body>
</html>

以下のコードの説明としては各ボタンの親要素であるdivElementにクリックイベントを仕込み、イベントバブリングによりクリックされたボタンを特定して、ボタン毎にハンドリングしてsetImagesメソッドにimageの配列を渡して画像をレンダリングしてます。

イベントバブリングについては第2回の私の記事で詳しく解説しているので、是非ご覧下さい。

[promiseStaticDogApi.js]

const buttons = document.getElementById('buttons')
buttons.addEventListener('click', (e) => {
  const clickBtnId = e.target.id
  const selectedDogNames = [];
  const checkboxes = document.querySelectorAll('input[type="checkbox"]:checked');
  checkboxes.forEach((checkbox) => {
    const dogName = checkbox.value;
    selectedDogNames.push(`https://dog.ceo/api/breed/${dogName}/images/random`); 
  });
  if(/AllSubmitBtn/.test(clickBtnId)) promiseAll(selectedDogNames)
  if(/AllSettledSubmitBtn/.test(clickBtnId)) promiseAllSettled(selectedDogNames)
  if(/RaceSubmitBtn/.test(clickBtnId)) promiseRace(selectedDogNames)
  if(/AnySubmitBtn/.test(clickBtnId)) promiseAny(selectedDogNames)
});

const promiseAll = (selectedDogNames) => {
  Promise.all(selectedDogNames.map(fetchDogImage))
      .then(imageUrlsArr => setImages(imageUrlsArr))
      .catch(error => alert(`promiseAllエラー: ${error}`));
}

const promiseAllSettled = (selectedDogNames) => {
  Promise.allSettled(selectedDogNames.map(fetchDogImage))
    .then(results => {
      const imageUrlsArr = results
        .filter(result => result.status === 'fulfilled')
        .map(result => result.value);
      const failed = results.filter(result => result.status === 'rejected');
      if (failed.length > 0) console.error('images failed:', failed);
      setImages(imageUrlsArr);
    });
}

const promiseRace = (selectedDogNames) => {
  Promise.race(selectedDogNames.map(fetchDogImage))
    .then(imageUrl => setImages([imageUrl]))
    .catch(error => console.error("Error in promise race:", error));
}

const promiseAny = (selectedDogNames) => {
  Promise.any(selectedDogNames.map(fetchDogImage))
    .then(imageUrl => setImages([imageUrl]))
    .catch(error => console.dir(error));
}

const fetchDogImage = (url) => {
  return new Promise((resolve, reject) => {
    fetch(url)
      .then((res) => {
        if (!res.ok) return reject(new Error(`url: ${url}. Status: ${res.status}`))
        return res.json()
      })
      .then((data) => resolve(data.message))
      .catch((error) => reject(new Error(`fetchDogImage error: ${error}`)));
  });
};

const setImages = (images) => {
  const imagesElem = document.getElementById('images');
  images.forEach((image) => {
    imagesElem.insertAdjacentHTML(
      "beforeend",
      `<img src="${image}" alt="" style="height:200px; width:200px;">`
    );
    setTimeout(() => {
      imagesElem.lastChild.remove();
    }, 3000);
  });
};

Promise.allボタンの挙動

上記で解説した通り、Promise.allは渡されたpromiseが全部resolveならresolveを返し、一つでもrejectなら最初のreject値を返します。なので挙動としては犬の名前のみ選んでいる場合は正常に動作し、犬以外の名前を一つでも選んでいる場合はエラーになります。

const promiseAll = (selectedDogNames) => {
  Promise.all(selectedDogNames.map(fetchDogImage))
      .then(imageUrlsArr => setImages(imageUrlsArr))
      .catch(error => alert(`promiseAllエラー: ${error}`));
}
  • 以下の様に犬の名前を選んで「promiseAll送信」ボタンを押すと犬の写真がレンダリングされます。

  • 犬の名前以外を一つでも選ぶと画像はレンダリングされず以下のようにrejectされcatchに入ります。

Promise.allSettledボタンの挙動

上記で解説した通り、Promise.allSettledは引数のpromiseがrejectされてもresolveされます。

  • 以下コードはPromise.allSettledのthenの返り値でfulfilledのプロパティを持っている値をfilterしsetImagesメソッドに渡し、rejectedのプロパティを持っているものをエラーとしてconsoleに出すようにしています。
const promiseAllSettled = (selectedDogNames) => {
  Promise.allSettled(selectedDogNames.map(fetchDogImage))
    .then(results => {
      const imageUrlsArr = results
        .filter(result => result.status === 'fulfilled')
        .map(result => result.value);
      const failed = results.filter(result => result.status === 'rejected');
      if (failed.length > 0) console.error('images failed:', failed);
      setImages(imageUrlsArr);
    });
}
  • Promise.allSettledのthenの返り値

  • 以下の様に犬じゃないものにチェックをつけても犬の写真は問題なくレンダリングされているのが分かります。

  • rejectedのエラーもconsoleに表示されている。

Promise.raceボタンの挙動

上記で解説した通り一番初めにresolveかrejectしたものだけを対応します。

const promiseRace = (selectedDogNames) => {
  Promise.race(selectedDogNames.map(fetchDogImage))
    .then(imageUrl => setImages([imageUrl]))
    .catch(error => console.error("Error in promise race:", error));
}

resolve時

reject

Promise.anyボタンの挙動

  • 上記で解説した通りrejectは無視し、一番初めにresolveされたもののみ対応します。ただ、全部rejectならcatchに入ります。
const promiseAny = (selectedDogNames) => {
  Promise.any(selectedDogNames.map(fetchDogImage))
    .then(imageUrl => setImages([imageUrl]))
    .catch(error => console.dir(error));
}
  • 犬の名前とラーメンを選択してもresolveされた一番最初の犬の写真が出てきます。

  • 犬以外の名前のみ選ぶと全部rejectされるのでchtchに入ってエラーが出ているのが分かります。

fetchDogImage関数を()をつけて渡さない理由

個人的にはjsを学び始めた時、分かり辛かったので念の為解説します。

各非同期メソッドでは以下のPromise.allの様にfetchDogImageメソッドを()を付けずに渡しています。これはreferenceとして渡すためです。

const imageUrlsArr = await Promise.all(selectedDogNames.map(fetchDogImage)); 

JavaScriptにおいて、関数はオブジェクトの一種であり、変数に代入したり、他の関数に引数として渡すことができます。fetchDogImageは関数への参照であり、その関数自体を呼び出すためには ()を付ける必要があります。

しかし、このコードでは fetchDogImage自体を呼び出すのではなく、selectedDogNames 配列の各要素に対してmap メソッドを使って fetchDogImage を適用することが目的です。

このようにmapメソッドに渡すコールバック関数(fetchDogImageメソッド)を()なしで渡すことで、mapメソッド自体がselectedDogNames配列の各要素に対してfetchDogImageを適用し、それらの結果を新しい配列にしてくれます。

なので以下の1と2は同じ意味です。

配列の各要素にメソッドを適用したい場合は1の様に省略して書けるので、是非使ってみて下さい

1, selectedDogNames.map(fetchDogImage)
2, selectedDogNames.map(value => fetchDogImage(value))

Async Awaitに書き換え

  • 以下のような形でasync awaitに書き換えたverも作ってみました。

[promiseStaticDogApi.js]

const buttons = document.getElementById('buttons')
buttons.addEventListener('click', (e) => {
  const clickBtnId = e.target.id;
  const selectedDogNames = [];
  const checkboxes = document.querySelectorAll('input[type="checkbox"]:checked');
  checkboxes.forEach((checkbox) => {
    const dogName = checkbox.value;
    selectedDogNames.push(`https://dog.ceo/api/breed/${dogName}/images/random`);
  });
  if (/AllSubmitBtn/.test(clickBtnId)) promiseAll(selectedDogNames);
  if (/AllSettledSubmitBtn/.test(clickBtnId)) promiseAllSettled(selectedDogNames);
  if (/RaceSubmitBtn/.test(clickBtnId)) promiseRace(selectedDogNames);
  if (/AnySubmitBtn/.test(clickBtnId)) promiseAny(selectedDogNames);
});

const promiseAll = async (selectedDogNames) => {
  try {
    const imageUrlsArr = await Promise.all(selectedDogNames.map(fetchDogImage));
    setImages(imageUrlsArr);
  } catch (error) {
    alert(`promiseAllエラー: ${error.message}`);
  }
}

const promiseAllSettled = async (selectedDogNames) => {
  const results = await Promise.allSettled(selectedDogNames.map(fetchDogImage));
  const imageUrlsArr = results
    .filter(result => result.status === 'fulfilled')
    .map(result => result.value);
  const failed = results.filter(result => result.status === 'rejected');
  if (failed.length > 0) console.error('images failed:', failed.map(failedVal => failedVal.reason.message));
  setImages(imageUrlsArr);
}

const promiseRace = async (selectedDogNames) => {
  try {
    const imageUrl = await Promise.race(selectedDogNames.map(fetchDogImage));
    setImages([imageUrl]);
  } catch (error) {
    console.error("Error in promise race:", error.message);
  }
}

const promiseAny = async (selectedDogNames) => {
  try {
    const imageUrl = await Promise.any(selectedDogNames.map(fetchDogImage));
    setImages([imageUrl]);
  } catch (error) {
    console.dir(error);
  }
}

const fetchDogImage = async (url) => {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`url: ${url}. Status: ${res.status}`);
  const data = await res.json();
  return data.message;
};

const setImages = (images) => {
  const imagesElem = document.getElementById('images');
  images.forEach((image) => {
    imagesElem.insertAdjacentHTML(
      "beforeend",
      `<img src="${image}" alt="" style="height:200px; width:200px;">`
    );
    setTimeout(() => {
      imagesElem.lastChild.remove();
    }, 3000);
  });
};

まとめ


promiseの静的メソッドを理解出来たら、もう一段階、非同期処理を高いレベルで使いこなせるかなって思いました。 特にループ内で非同期処理を行いたい場合はforEachやmapは使えないのでforかfor ofを使うと思います。 ループを回す順番でのハンドリングが必要なければPromise.allやallSettledなどを使った方が一気に処理してくれるのでとてもオススメ出来るかなって思いました。

参考資料