かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript (ES2015) 今更のPromise (3) 非同期処理を順番に実行するチョットマニアックなメモ

前回までのあらすじ

非同期処理を平行に実行する場合はPromise.allPromise.raceを使えばよいのですが、非同期処理で受け取った値を使って次の処理を行いたいなど順番(同期的)に実行したい場合のメモ

Promise~thenでのメソッドチェーン

たぶん一番正当なやり方。
Promiseオブジェクトを返す関数を実行してresolveされた時に実行されるthenメソッド内で、Promiseオブジェクトを返す関数をreturnすることで、Promise.prototype.thenのメソッドチェーンができる。
エラーになるとcatchに処理が流れる。

実行される関数

function myFunc(a, b = 0) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if(a) {
        let res = (a + b) * 2
        console.log(`(${a} + ${b}) * 2 = ${res}`)
        resolve( res )
      } else {
        reject(`Error a is ${a}`)
      }
    }, 10)
  })
}

function onReject(error) {
  console.log('>> onReject', error)
}

Promise.prototype.thenのメソッドチェーン

myFunc(1).then((res1) => {
  // res1 => 2
  return myFunc(2, res1)
})
.then((res2) => {
  // res2 => 8
  return myFunc(3, res2)
})
.then((res3) => {
  // res3 => 22
  console.log('> Promaise Complete', res3)
})
.catch(onReject)
// => (1 + 0) * 2 = 2
// => (2 + 2) * 2 = 8
// => (3 + 8) * 2 = 22
// => > Promaise Complete 22

エラーになる場合

myFunc(1).then((res1) => {
  // res1 => 2
  return myFunc(0, res1) // => reject になる
})
.then((res2) => {
  return myFunc(3, res2)
})
.then((res3) => {
  console.log('> Promaise Complete', res3)
})
.catch(onReject)
// => (1 + 0) * 2 = 2
// => >> onReject error a is 0

エラーが発生するとそれ以降のthenはスキップされる。
わかりやすいですね!

promise.then(successCallback, failureCallback) の場合

Promiseを使う | MDNに書かれているpromise.then(successCallback, failureCallback)で同期的に実行させようとする場合。

myFunc(1).then((res1) => {
  myFunc(2, res1).then((res2) => {
    myFunc(3, res2).then((res3) => {
      console.log('> Promaise Complete', res3)
    }, (error) => console.log('Error 3', error))
  }, (error) => console.log('Error 2', error))
}, (error) => console.log('Error 1', error))
// => (1 + 0) * 2 = 2
// => (2 + 2) * 2 = 8
// => (3 + 8) * 2 = 22
// => > Promaise Complete 22

// Errorになる場合
myFunc(1).then((res1) => {
  myFunc(0, res1).then((res2) => {
    myFunc(3, res2).then((res3) => {
      console.log('> Promaise Complete', res3)
    }, (error) => console.log('Error 3', error))
  }, (error) => console.log('Error 2', error))
}, (error) => console.log('Error 1', error))
// => (1 + 0) * 2 = 2
// => Error 2 Error a is 0

エラーの場合は先のthenをメソッドチェーンした場合と同じで、エラーになるとそれ以降の処理には流れず、エラーの時点で処理は完了になります。

thenの第一引数内で次のPromiseオブジェクトを返す関数を実行する必要があるので、同期処理が多くなるとコードのようにネストして見通しがあまり良くない感じです。ただ、エラーの処理をそれぞれ別に設定できるのはメリットかも知れません。

promise.then(successCallback, failureCallback) でthenのメソッドチェーンにした場合

ここからチョットマニアックかも。

myFunc(1)
.then(
  (res1) => myFunc(0, res1),
  (error) => console.log('Error 1', error)
)
.then(
  (res2) => myFunc(3, res2),
  (error) => console.log('Error 2', error)
)
.then(
  (res3) => console.log('> Promaise Complete', res3),
  (error) => console.log('Error 3', error)
)
.catch(onReject)
// => (1 + 0) * 2 = 2
// => Error 2 Error a is 0
// => > Promaise Complete undefined

上の例では、

  1. 1つめのthen: successCallback => muFunc(0, res1) (res1は2)
  2. 2つめのthen: failureCallback => console.log('Error 2', error)
  3. 3つめのthen: successCallback => console.log('> Promaise Complete', res3) (res3はundefined)

thenの第二引数にfailureCallbackが渡されていると、エラーの時にcatchに流れるのではなく、failureCallbackが呼ばれ、メソッドチェーンしてる次のthenに処理が流れるようです。

次のthenに流れた時に渡される引数について

先の例ではエラーになった時のコールバックではreturn console.log('Error 2')が実行されundefinedが返されているので、エラーで次のthenに渡される引数が「常にundefined」なのか「コールバック関数で返された値」がなのかが判断しきれません。

そこで、明示的に値を返して試してみます。

myFunc(0)
.then(
  (res1) => myFunc(2, res1),
  (error) => {
    console.log('Error 1')
    return 'Foo'
})
.then(
  (res2) => console.log('> Promise Complete:', res2),
  (error) => console.log('Error2', error)
)
// => Error 1
// => > Promise Complete: Foo

どうやら、エラーコールバックで返される(returnされる)値が次のthen内のsuccessCallbackに渡され実行されるようです。

Promise.reject()を返せば次のthenはエラーコールバックの方が呼ばれる

明示的にreturn Promise.reject()すると、次のthenの処理はエラーコールバックfailureCallbackの方に処理が流れました。

myFunc(0).then(
  (res1) => myFunc(2, res1),
  (error) => {
    console.log('Error 1')
    // Promise.reject を返す
    return Promise.reject('Reject 1')
})
.then(
  (res2) => console.log('> Promise Complete:', res2),
  (error) => console.log('Error2', error)
)
// => Error 1
// => Error2 Reject 1

 
promise.then(successCallback, failureCallback).then(...)の形式でメソッドチェーンをした場合は、

  • エラーの場合はcatchではなく、第二引数のfailureCallbackが実行される
  • 先の処理の成功失敗に関わらず、次のthenが呼ばれる
  • 次のthenのコールバックに渡される値は、先のコールバック内でreturnされた値
  • 次のthenでエラーに処理を流したい場合は、return Promise.reject("エラーコールバックに渡す引数")とする

という感じっぽいです。

先の非同期処理が成功しようが失敗しようが処理を待って次の処理を実行することができるので、値があれば値を使って、無ければ無いなりで必須な共通処理とか使い方はあるかもしれません。

myFunc(0)
.then(
  (res1) => {
    console.log('Success 1: res1 is ', res1)
    return myFunc(res1)
  },
  (error1) => {
    console.log('Error 1: default val 1')
    return 1
  }
)
.then(
  (res2) => {
    // 前のthenでPromise.rejectされないので、必ずココが実行される
    console.log('Common callback: res2 is', res2)
    return myFunc(res2)
  },
  (error2) => {
    console.log('Error 2:', error2)
    return Promise.reject('ERROR 2')
  }
)
.then(
  (res3) => {
    console.log('Promise complete:', res3)
  },
  (error) => {
    console.log('Error3', error)
  }
)
// => Error 1: default val 1
// => Common callback: res2 is 1
// => (1 + 0) * 2 = 2
// => Promise complete: 2

途中からthen~catchのメソッドチェーンにした場合

TASK

function myFunc2(a, b = 0) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if(!a) {
        reject(`Error a is ${a}`)
      }
      let res = (a + b) * 2
      console.log(`(${a} + ${b}) * 2 = ${res}`)
      resolve( res )
    }, 10)
  })
}

1. 成功した場合は、そのまま次のthenが実行される

myFunc2(1)
.then(
  (res1) => {
    console.log('Success 1: res1 is ', res1)
    return myFunc(2, res1)
  },
  (error1) => {
    console.log('Error 1')
    return 2
  }
)
.then(
  (res2) => {
    console.log('Success 2: res2 is ', res2)
  }
)
.catch(
  (error) => {
    console.log("Error catch", error)
  }
)
// => (1 + 0) * 2 = 2
// => Success 1: res1 is  2
// => (2 + 2) * 2 = 8
// => Success 2: res1 is  8

2. 失敗し、コールバック関数内で単に値をreturnした場合も、そのまま次のthenが実行される

myFunc2(0)
.then(
  (res1) => {
    console.log('Success 1: res1 is ', res1)
    return myFunc(2, res1)
  },
  (error1) => {
    console.log('Error 1')
    return 2
  }
)
.then(
  (res2) => {
    console.log('Success 2: res2 is ', res2)
  }
)
.catch(
  (error) => {
    console.log("Error catch", error)
  }
)
// => (0 + 0) * 2 = 0
// => Error 1
// => Success 2: res2 is  1 ... 先のエラー関数でreturnされた値が渡される

3. 失敗し、コールバック関数内でPromise.reject()を返した場合、catchに処理が流れる

myFunc2(0)
.then(
  (res1) => {
    console.log('Success 1: res1 is ', res1)
    return myFunc(2, res1)
  },
  (error1) => {
    console.log('Error 1')
    return Promise.reject("Reject!")
  }
)
.then(
  (res2) => {
    console.log('Success 2: res2 is ', res2)
  }
)
.catch(
  (error) => {
    console.log("Error catch", error)
  }
)
// => (0 + 0) * 2 = 0
// => Error 1
// => Error catch Reject! ... 先のコールバック関数で呼ばれたPromise.rejectに渡された値が引数で渡される

4. 成功したコールバック関数内でPromise.rejectが返された場合もcatchに処理が流れる

myFunc2(1)
.then(
  (res1) => {
    console.log('Success 1: res1 is ', res1)
    // 成功したけど、reject
    return Promise.reject(`res 1 is ${res1}`)
  },
  (error1) => {
    console.log('Error 1')
    return Promise.reject("Reject!")
  }
)
.then(
  (res2) => {
    console.log('Success 2: res2 is ', res2)
  }
)
.catch(
  (error) => {
    console.log("Error catch", error)
  }
)
// => (1 + 0) * 2 = 2
// => Success 1: res1 is  2
// => Error catch res 1 is 2

途中からthen~catchのメソッドチェーンを繋ぐことは問題ないようなので、successCallback, failureCallbackを使った場合の特性を知っていれば、特定のエラー処理だけ分けたいとかって場合などに使えるかもです。(あまりお行儀はよくなさそうだけど...)
 
さっさとawait /asyncに行けばいいのに色々と気になって試してしまいました。(学習が遅い言い訳


これから学ぶ JavaScript

これから学ぶ JavaScript