👹

大嫌いだったPromiseの基礎から実践まで - 備忘録

に公開

Promiseの基礎から実践まで - 備忘録

Promiseってプログラミング初心者にとっては、強敵ですよね、、、。
大抵、Promiseと非同期通信で一度、大ダメージを受けると思います。当然、私もその一人だったんですが、最近、いろんな書籍や、動画、記事などを読んでいく中で、だいぶ理解が深まり、言語化できそだなと思ったので、忘れないように記事にしておこうと思います!!

まず、参考文献と致しまして、以下のakiさんの記事を呼んで、感動しました!
この記事のおかげで、なるほど!!Promiseってそういうことか!?って初めて実感できたのを今でも覚えています✨️こういうわかりやすい記事を作れるってすごいですよね(^^)

https://zenn.dev/ak/articles/dc23436b241a84

ということでこの記事の説明の仕方を模倣させていただきつつ、もう少し深堀りして調べた内容などを記事にまとめていこうと思います🔥

そもそも非同期処理とは?

Promiseについて知るためには、まず非同期処理について知っておく必要があります。

非同期処理っていうのは、簡単に言うと「待ち時間がある処理を、待たずに次の処理を進める」仕組みのことです。

例えば、カフェでコーヒーを注文するとき、注文したらその場でずっと待つんじゃなくて、席に座って他のことをしながら待ちますよね。そして、コーヒーができたら呼ばれて受け取りに行く。これが非同期処理のイメージです。

プログラミングでも同じで、サーバーからデータを取ってくるような「時間がかかる処理」を待っている間、他の処理を進められるようにするのが非同期処理です。

Promiseとは

では、本題です。
Promiseとは、ES2015で導入された、非同期処理の状態や結果を表現するオブジェクトのことです。

例えば、出前アプリでピザを注文することをイメージしてみましょう。

ピザを注文すると、アプリの中で「準備中」か「配達済み」か「キャンセルされたか」の3つの状態を教えてくれますよね。

これと同じように、Promiseも3つの状態を持ちます。

具体的に言うと、まず最初はpendingという保留中の状態になって、成功したら、fulfilledと呼ばれる状態になります。そして、もし失敗や例外が起こったらrejectedと呼ばれる状態になります。

このように、Promiseの仕組みを使えば、注文したピザの状態を追跡するように、非同期処理の状態や結果を追跡できます。

Promiseの基本的な使い方

では、実際にPromiseを使ってみましょう。Promiseを作成するには、以下のように書きます。

new Promise(() => {})

これを実行したら、以下のようなPromiseオブジェクトが作られます。

Promise {<pending>}

よく見たら、「pending」という文字が書かれていますね。これは、先ほど述べたとおり、Promiseの初期状態、つまり「保留中」を示しています。

では、この保留中の状態を「成功」や「失敗」に変えるには、どうすればよいでしょうか?

実は、Promiseのコールバック関数の中には、2つの引数を指定することができます。

1つ目の引数はresolveという関数で、これを呼ぶと作業が成功したということを示します。そして2つ目の引数は、rejectという関数で、これを呼ぶと作業が失敗したことを示します。

では、早速この2つの関数を実行してみましょう。

new Promise((resolve, reject) => resolve("配達された!"))
// => Promise {<fulfilled>: '配達された!'}

new Promise((resolve, reject) => reject("キャンセルされた!"))
// => Promise {<rejected>: 'キャンセルされた!'}

それぞれ、結果として「fulfilled」と「rejected」という文字が表示されたのが分かると思います。

重要なポイント - Promiseの状態は一方通行

ここで1つ重要なことがあります。Promiseの状態は一度変わったら、もう二度と変わりません

ピザで例えると、一度「配達済み」になったら、その後に「準備中」に戻ったりしないですよね。それと同じです。

new Promise((resolve, reject) => {
  resolve("配達された!")
  reject("キャンセルされた!") // これは無視される
})
// => Promise {<fulfilled>: '配達された!'}

最初にresolveが呼ばれた時点で状態が確定するので、その後のrejectは無視されます。
これ、結構重要なポイントだと思ってます。

thenとcatchの使い方

では、次に「then」と「catch」の使い方について解説していきます。

thenメソッドは、簡単に言うと、「成功したときはこうする」という指示をプログラムに伝える方法のことです。catchメソッドは、逆に「失敗したときはこうする」という指示を伝える方法のことです。

例えば、出前アプリで考えると、ピザが届いたら「美味しかった!」とレビューを送りたいですし、キャンセルされたら「返金してほしい!」とお願いしたいですよね。thenとcatchを使えば、こういった指示をプログラムに伝えることができます。

具体的には、以下のようなコードになります。

new Promise((resolve, reject) => resolve("配達された!"))
  .then((text) => console.log(text + "美味しかった!"))
// => "配達された!美味しかった!"

new Promise((resolve, reject) => reject("キャンセルされた!"))
  .catch((text) => console.log(text + "返金してほしい!"))
// => "キャンセルされた!返金してほしい!"

これで、成功したら「配達された!美味しかった!」と表示されますし、失敗したら「キャンセルされた!返金してほしい!」と表示されるようになります。

thenの第二引数でもエラーハンドリングできる

実は、thenメソッドには第二引数があって、そこでもエラーハンドリングができます。

promise.then(
  (v) => console.log("成功" + v),
  (e) => console.log("失敗" + e)
)

ただ、この書き方はおすすめしません。なぜかというと、catchを使った方が後で説明する「Promiseチェーン」で扱いやすいからです。基本的にはcatchを使いましょう。

finallyメソッドも覚えておこう

成功しても失敗しても、最後に必ず実行したい処理ってありますよね。
例えば、ローディング画面を消すとか。

そんなときに使えるのがfinallyメソッドです。

promise
  .then((v) => console.log("成功" + v))
  .catch((e) => console.log("失敗" + e))
  .finally(() => console.log("処理が終わりました"))

finallyは成功しても失敗しても必ず実行されます。ピザで例えると、「配達されても、キャンセルされても、とにかく注文は終了です」みたいな感じですね。

Promiseチェーン - 複数の処理を繋げる

ここからが実践的な内容です。

thenメソッドは、実は何個でも繋げることができます。これをPromiseチェーンと言います。

ピザの例で考えてみましょう。ピザが届いたら、まず箱を開けて、それから食べて、最後にレビューを書く。この一連の流れをPromiseチェーンで表現できます。

new Promise((resolve, reject) => {
  resolve("ピザが届いた")
})
  .then((result) => {
    console.log(result)
    return "箱を開けた"
  })
  .then((result) => {
    console.log(result)
    return "食べた"
  })
  .then((result) => {
    console.log(result)
    return "レビューを書いた"
  })

// 出力
// "ピザが届いた"
// "箱を開けた"
// "食べた"
// "レビューを書いた"

ここで超重要なポイントがあります。thenの中では必ずreturnしましょう

returnし忘れると、次のthenに値が渡らず、undefinedになってしまいます。
これ、初心者がよくハマるポイントなので要注意です。

// ❌ ダメな例
promise
  .then((result) => {
    console.log(result)
    "次に渡したい値" // returnがない!
  })
  .then((result) => {
    console.log(result) // => undefined
  })

// ✅ 良い例
promise
  .then((result) => {
    console.log(result)
    return "次に渡したい値" // returnする
  })
  .then((result) => {
    console.log(result) // => "次に渡したい値"
  })

Promiseチェーンの中でエラーが起きたら

Promiseチェーンの途中でエラーが起きると、catchまでスキップされます。

new Promise((resolve, reject) => {
  resolve("ピザが届いた")
})
  .then((result) => {
    console.log(result)
    throw new Error("箱が開けられない!")
  })
  .then((result) => {
    console.log("これは実行されない")
  })
  .catch((error) => {
    console.log("エラー" + error.message)
  })

// 出力
// "ピザが届いた"
// "エラー箱が開けられない!"

ピザで例えると、「届いた」→「箱を開けようとしたけど失敗」→「食べる処理はスキップ」→「エラー対応」という流れですね。

catchの後にもthenを繋げられる

実は、catchでエラーを処理した後、またthenで処理を続けることもできます。

promise
  .then(() => {
    throw new Error("エラー発生")
  })
  .catch((error) => {
    console.log("エラーを処理しました")
    return "復旧しました"
  })
  .then((result) => {
    console.log(result) // => "復旧しました"
  })

これは「エラーが起きたけど、対処して処理を続ける」ようなケースで使えます。

実践編1 - fetch APIで使ってみよう

ここからは実際のコードでPromiseを使ってみます♪

一番よく使うのが、当然、サーバーからデータを取ってくるfetch APIですよね。

fetch("https://api.example.com/users")
  .then((response) => {
    console.log("データを取得しました")
    return response.json() // JSONに変換
  })
  .then((data) => {
    console.log("ユーザー情報", data)
  })
  .catch((error) => {
    console.log("エラーが発生しました", error)
  })
  .finally(() => {
    console.log("通信が終了しました")
  })

fetchの裏側で何が起きているの?

ここでちょっと立ち止まって、fetchの裏側を見てみましょう。

fetch("https://api.example.com/users")を実行すると、実はこんなことが起きています。

// fetchの中身のイメージ(実際のコードではないけど、こんな感じ)
function fetch(url) {
  return new Promise((resolve, reject) => {
    // サーバーにリクエストを送る
    // ...通信中...
    
    // 通信が成功したら(if文などを使って)
    resolve({
      status: 200,
      ok: true,
      json: () => { /* JSONを返す処理 */ },
      // その他responseオブジェクトのプロパティ
    })
    
    // 通信が失敗したら(elseなどを使って)
    // reject(new Error("ネットワークエラー"))
  })
}

つまり、fetchは内部でPromiseを作って返してくれているんですね。

だから、処理の流れはこうなります。

  1. fetch("https://api.example.com/users")でPromiseオブジェクトが返ってくる(最初はpending状態)
  2. サーバーとの通信が成功したら、内部でresolve(responseオブジェクト)が呼ばれる
  3. Promiseがfulfilled状態になる
  4. resolveに渡されたresponseオブジェクトが次のthenの引数に渡される
  5. だから.then((response) => ...)のresponseには、そのresponseオブジェクトが入っている

ピザで例えると、「ピザを注文する」→「配達員が運んでくる(通信中)」→「玄関に到着(resolve)」→「受け取る(then)」という流れですね。

なぜthenを2回繋げるの?

さっきのコードを見て、「なんでthenが2回あるの?」って思いませんでしたか?

fetch("https://api.example.com/users")
  .then((response) => response.json()) // 1回目
  .then((data) => console.log(data))   // 2回目

実は、response.json()もPromiseを返すんです。

だから、fetchはPromiseの中にPromiseがある二段構えになっているんですね。

fetch("https://api.example.com/users")
  // ↓ 1回目のthen: responseオブジェクトを受け取る
  .then((response) => {
    return response.json() // これもPromiseを返す!
  })
  // ↓ 2回目のthen: json()の結果(実際のデータ)を受け取る
  .then((data) => {
    console.log(data) // ここでやっと実際のデータが使える
  })

1回目のthenでは「通信の結果」を受け取って、2回目のthenでは「JSONに変換した実際のデータ」を受け取る。この2段階が必要なんです。

ピザで例えると、「ピザが届いた(1回目のthen)」→「箱を開けて中身を取り出す(json())」→「実際のピザを食べる(2回目のthen)」みたいな感じですね。

補足:Responseオブジェクトの中身を理解する

ここで、もう少し深掘りしておきます。
1回目のthenで受け取るresponseオブジェクトには、実はbodyというプロパティがあります。

でも、このbodyには直接JSONデータが入っているわけではありません。
bodyReadableStreamという形式で、まだ「生のデータ」の状態なんです。

なぜこんな仕組みになっているかというと、

  • レスポンスが大きい場合、一度に全部メモリに読み込むと負荷がかかる
  • ストリーミング形式で少しずつ読み取れるようにするため
  • データの形式(JSON、テキスト、Blobなど)が確定していないため

そこでjson()メソッドの出番です。

json()JavaScriptの標準API(Fetch APIの一部)として用意されているメソッドで、Responseオブジェクトに対して使えます。

このメソッドは、

  1. bodyのストリームを最後まで読み取る(非同期処理)
  2. 読み取ったテキストをJSONとしてパースする
  3. パース結果のJavaScriptオブジェクトでresolveするPromiseを返す

だから、response.json()も非同期処理であり、Promiseを返すんですね。

私の中では、「json()はJavaScriptの標準搭載メソッドで、Responseオブジェクトのデータをjsonとして最後まで読み取る非同期関数」と覚えておこうと思います!

ちなみに、Responseオブジェクトには他にも似たメソッドがあります!

  • response.text() - テキストとして読み取る
  • response.blob() - Blobとして読み取る

これらもすべて非同期でPromiseを返します。
データの形式に応じて使い分けることができるんですね(^^)

エラーハンドリングのベストプラクティス

fetch APIを使うときは、ネットワークエラーだけじゃなくて、HTTPステータスエラー(404とか500とか)もちゃんと処理したいですよね。

fetch("https://api.example.com/users")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTPエラー ステータス${response.status}`)
    }
    return response.json()
  })
  .then((data) => {
    console.log("成功", data)
  })
  .catch((error) => {
    console.log("エラー", error.message)
  })

response.okをチェックすることで、ステータスコードが200番台かどうか確認できます。ダメだったらエラーを投げて、catchで受け取る。これが王道パターンだと勝手に思っています笑。

実践編2 - 複数の非同期処理を扱う

複数の非同期処理を扱うときに便利なメソッドを紹介します。

Promise.all - 全部成功したら次に進む

複数のAPIを同時に呼んで、全部の結果が揃ってから次の処理をしたいときがあります。
そんなときはPromise.allを使います。

const promise1 = fetch("https://api.example.com/users")
const promise2 = fetch("https://api.example.com/posts")
const promise3 = fetch("https://api.example.com/comments")

Promise.all([promise1, promise2, promise3])
  .then((results) => {
    console.log("全部のデータが揃いました", results)
    // results[0] がusers
    // results[1] がposts
    // results[2] がcomments
  })
  .catch((error) => {
    console.log("どれか1つでも失敗しました", error)
  })

Promise.allは、全部のPromiseが成功したら結果を配列で返してくれます。
でも、1つでも失敗したらすぐにcatchに飛ぶので注意が必要です。

ピザで例えると、「ピザとサラダとドリンクを同時注文して、全部揃ったら食べ始める。1つでもキャンセルされたら全部キャンセル」みたいな感じですね。

Promise.allSettled - 成功しても失敗しても全部待つ

「1つ失敗しても他のは使いたい」ってときもありますよね。
そんなときはPromise.allSettledを使います。

const promise1 = fetch("https://api.example.com/users")
const promise2 = fetch("https://api.example.com/404") // これは失敗する
const promise3 = fetch("https://api.example.com/posts")

Promise.allSettled([promise1, promise2, promise3])
  .then((results) => {
    results.forEach((result, index) => {
      if (result.status === "fulfilled") {
        console.log(`${index}番目は成功`, result.value)
      } else {
        console.log(`${index}番目は失敗`, result.reason)
      }
    })
  })

allSettledは必ず全部のPromiseを待って、それぞれの結果(成功か失敗か)を教えてくれます。

Promise.race - 一番早いものだけ採用

複数のPromiseの中で、一番早く終わったものだけ使いたいときはPromise.raceを使います。

const promise1 = new Promise((resolve) => setTimeout(() => resolve("1秒後"), 1000))
const promise2 = new Promise((resolve) => setTimeout(() => resolve("2秒後"), 2000))
const promise3 = new Promise((resolve) => setTimeout(() => resolve("3秒後"), 3000))

Promise.race([promise1, promise2, promise3])
  .then((result) => {
    console.log("一番早かったのは", result) // => "1秒後"
  })

これは例えば、「複数のサーバーに同じリクエストを投げて、一番早く返ってきたものを使う」みたいなケースで使えます。

実践編3 - async/awaitとの関係

最後に、async/awaitとの関係について説明します。

async/awaitは、Promiseをもっと読みやすく書くための構文です。
実は裏側ではPromiseを使っています。

// Promiseで書いた場合
fetch("https://api.example.com/users")
  .then((response) => response.json())
  .then((data) => {
    console.log(data)
  })
  .catch((error) => {
    console.log(error)
  })

// async/awaitで書いた場合
async function getUsers() {
  try {
    const response = await fetch("https://api.example.com/users")
    const data = await response.json()
    console.log(data)
  } catch (error) {
    console.log(error)
  }
}

getUsers()

async/awaitを使うと、まるで同期処理みたいに書けるので、コードが読みやすくなります。

でも、Promiseの仕組みを理解していないとasync/awaitも理解できないので、まずはPromiseをしっかり理解することが大事です。

awaitの本質を理解する

awaitは**「Promiseが解決するまで待つ」**という機能です。

// thenで書いた場合
fetch("https://api.example.com/users")
  .then((response) => {
    console.log(response) // responseが手に入る
  })

// async/awaitで書いた場合
const response = await fetch("https://api.example.com/users")
console.log(response) // responseが手に入る

thenの場合は「成功したらこの関数を実行してね」ってコールバック関数を渡しています。

一方、awaitは「ここで待って、結果が出たら変数に入れて次に進んで」という同期処理っぽい書き方ができます。

ピザで例えると、thenは「ピザが届いたら電話してください。そしたら取りに行きます」という感じで、awaitは「ここで待ってます。届いたら教えてください」という感じですね。

Promiseチェーンをasync/awaitに変換してみる

段階的に見ていきましょう。こんなPromiseチェーンがあったとします。

// Promiseチェーン
fetch("https://api.example.com/users")
  .then((response) => {
    console.log("1. レスポンス取得")
    return response.json()
  })
  .then((data) => {
    console.log("2. JSON変換完了")
    return data.filter(user => user.age > 20)
  })
  .then((adults) => {
    console.log("3. フィルタリング完了", adults)
  })

これをasync/awaitで書くと、こうなります。

async function getAdultUsers() {
  const response = await fetch("https://api.example.com/users")
  console.log("1. レスポンス取得")
  
  const data = await response.json()
  console.log("2. JSON変換完了")
  
  const adults = data.filter(user => user.age > 20)
  console.log("3. フィルタリング完了", adults)
}

ポイントは次の3つです。

  1. 各thenの中でreturnしていた値が、awaitの左側の変数に入る
  2. thenのネストがなくなって、上から下に読める
  3. でも、やってることは全く同じ

thenだと「次のthenに渡す値をreturnする」という書き方でしたが、awaitだと「結果を変数に入れる」という書き方になります。こっちの方が直感的ですよね。

エラーハンドリングの変換

エラーハンドリングの変換が一番分かりづらいかもしれません。

// Promiseのcatch
fetch("https://api.example.com/users")
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.log("エラー", error))

これをasync/awaitで書くと、こうなります。

async function getUsers() {
  try {
    const response = await fetch("https://api.example.com/users")
    const data = await response.json()
    console.log(data)
  } catch (error) {
    console.log("エラー", error)
  }
}

catchがtry-catchに置き換わりましたね。

なぜかというと、awaitでエラーが起きると、そのエラーがthrowされるからです。だからtry-catchで受け取る必要があるんです。

// awaitでエラーが起きると...
try {
  const response = await fetch("存在しないURL")
  // ↑ ここでエラーが起きると、自動的にthrowされる
} catch (error) {
  // ↑ throwされたエラーをここでキャッチできる
  console.log("エラー", error)
}

Promiseのcatchと、JavaScriptの通常のtry-catchが繋がるわけですね。これがasync/awaitの便利なところです。

async関数の3つの重要ポイント

async/awaitを使いこなすために、async関数の本質を理解しておきましょう。

async関数には、絶対に覚えておくべき3つの重要なポイントがあります。

ポイント1: async関数は必ずPromiseを返す

async関数は、どんな場合でも必ずPromiseオブジェクトを返します

async function test() {
  return "完了"
}

console.log(test())
// => Promise {<fulfilled>: "完了"}

普通の関数のように値を返しているように見えますが、実際にはその値でresolveされたPromiseが返ってきています。

だから、async関数の戻り値は以下のように使えます。

async function greet() {
  return "こんにちは"
}

// Promiseなので、thenが使える
greet().then((message) => {
  console.log(message) // => "こんにちは"
})

// もちろんawaitでも受け取れる
const message = await greet()
console.log(message) // => "こんにちは"

ポイント2: 関数が値を返せば、Promiseはその値でresolveする

async関数の中でreturnした値は、自動的にresolve(返す値)として扱われます。

// async関数で書いた場合
async function getUserName() {
  return "太郎"
}

// これは実質的に以下と同じ
function getUserName() {
  return new Promise((resolve) => {
    resolve("太郎")
  })
}

// どちらも同じ結果
getUserName().then((name) => {
  console.log(name) // => "太郎"
})

もう少し実践的な例を見てみましょう。

async function fetchUserData() {
  const response = await fetch("https://api.example.com/user/1")
  const data = await response.json()
  
  // ここでreturnすると、自動的にresolve(data)になる
  return data
}

// だから、このように使える
fetchUserData()
  .then((userData) => {
    console.log("ユーザーデータ取得成功", userData)
  })

ポイント3: 関数がエラーをthrowした場合、Promiseはそのエラーでrejectする

async関数の中でthrowすると、自動的にreject(エラー)として扱われます。

// async関数で書いた場合
async function riskyOperation() {
  throw new Error("エラーが発生しました")
}

// これは実質的に以下と同じ
function riskyOperation() {
  return new Promise((resolve, reject) => {
    reject(new Error("エラーが発生しました"))
  })
}

// どちらも同じ結果
riskyOperation()
  .catch((error) => {
    console.log(error.message) // => "エラーが発生しました"
  })

実践的な例も見てみましょう。

async function fetchUserData(userId) {
  // ユーザーIDのバリデーション
  if (!userId) {
    // throwすると、自動的にreject(new Error(...))になる
    throw new Error("ユーザーIDが指定されていません")
  }
  
  const response = await fetch(`https://api.example.com/user/${userId}`)
  
  // HTTPエラーのチェック
  if (!response.ok) {
    throw new Error(`HTTPエラー: ${response.status}`)
  }
  
  const data = await response.json()
  return data
}

// エラーハンドリング
fetchUserData(null)
  .catch((error) => {
    console.log(error.message) // => "ユーザーIDが指定されていません"
  })

// try-catchでも受け取れる
try {
  const userData = await fetchUserData(null)
} catch (error) {
  console.log(error.message) // => "ユーザーIDが指定されていません"
}

3つのポイントのまとめ

この3つのポイントを図にすると、こんな感じです。

async function example() {
  // 正常に値を返す → resolve(値)
  return "成功"
  
  // エラーを投げる → reject(エラー)
  throw new Error("失敗")
}

// どちらの場合でも、必ずPromiseが返ってくる
example() // => Promise {<fulfilled>: "成功"} または Promise {<rejected>: Error}

つまり、async関数は「Promiseを返す関数を、よりシンプルに書くための構文」なんですね。

この仕組みを理解していれば、async/awaitで書かれたコードがどのように動いているのか、裏側まで見通せるようになります。

よくハマるポイントまとめ

最後に、Promiseを使うときによくハマるポイントをまとめておきます。

1. thenの中でreturnし忘れ

// ❌ ダメな例
promise.then((result) => {
  const processed = result + "処理済み"
  // returnがない!
})

// ✅ 良い例
promise.then((result) => {
  const processed = result + "処理済み"
  return processed
})

2. Promiseコンストラクタ内のエラーは自動でrejectされる

これは知っておくと便利な機能です。以下の2つのコードは、実は全く同じ動きをします。

// パターン1: rejectを明示的に呼ぶ
new Promise((resolve, reject) => {
  reject(new Error("エラー"))
})
  .catch((error) => {
    console.log("エラーをキャッチ", error)
  })

// パターン2: エラーを投げる
new Promise((resolve, reject) => {
  throw new Error("エラー")
})
  .catch((error) => {
    console.log("エラーをキャッチ", error)
  })

つまり、Promiseのコンストラクタの中でthrowすると、JavaScriptが自動的に「あ、エラーが投げられたな。じゃあrejectを呼んであげよう」ってやってくれるんです。

実際のコードで使うケースを見てみましょう。

new Promise((resolve, reject) => {
  const result = 10 / 0 // 何か計算をする
  
  if (!isFinite(result)) {
    // パターンA: rejectを使う方法
    reject(new Error("計算結果が無限大です"))
    
    // パターンB: throwを使う方法(これでも同じ)
    throw new Error("計算結果が無限大です")
  }
  
  resolve(result)
})

どっちを使ってもいいんですが、使い分けるとしたら

  • 意図的にエラーにしたいときreject()を使う
  • 予期しないエラーが起きたときthrowが勝手にrejectにしてくれる

という感じですね。

ちなみに、thenの中でエラーを投げたときも同じように動きます。

Promise.resolve("成功")
  .then((result) => {
    throw new Error("途中でエラー") // これも自動的にrejectになる
  })
  .catch((error) => {
    console.log("キャッチできる", error)
  })

この仕組みがあるおかげで、予期しないエラーが起きても、ちゃんとcatchで拾えるようになっています。便利ですね。

3. catchの位置で挙動が変わる

// パターン1 - catchが最後
promise
  .then(() => "処理1")
  .then(() => "処理2")
  .catch(() => "全部のエラーをキャッチ")

// パターン2 - catchが途中
promise
  .then(() => "処理1")
  .catch(() => "処理1のエラーだけキャッチ")
  .then(() => "処理2") // ここは実行される

catchの位置によって、どこまでのエラーを捕まえるかが変わります。

4. Promise.allは1つでも失敗したら全部失敗

Promise.all([成功するPromise, 失敗するPromise, 成功するPromise])
  .then(() => {
    console.log("ここは実行されない")
  })
  .catch(() => {
    console.log("1つでも失敗したらここに来る")
  })

全部成功することが前提のときだけPromise.allを使いましょう。そうじゃない場合はPromise.allSettledを検討してください。

まとめ

Promiseは非同期処理を扱うための強力な仕組みです。

基本的な使い方から実践的な使い方まで見てきましたが、大事なポイントをまとめると

  • Promiseには3つの状態がある(pending、fulfilled、rejected)
  • 状態は一度変わったら変わらない
  • thenで成功時の処理、catchで失敗時の処理を書く
  • thenは繋げられる(Promiseチェーン)が、returnを忘れずに
  • 複数の非同期処理はPromise.allやPromise.allSettledで扱える
  • async/awaitはPromiseを読みやすく書くための構文

こんな感じですね。
読み手のことを考えず、超長文で、とにかく大事だと思ったことを詰め込みまくった記事になってしまいました(泣)
私の中では、かなり頭がスッキリしたので引き続き、学習頑張ります🔥

Discussion