JavaScript
promise
es6

ES6-Promiseをネストしてみる

More than 1 year has passed since last update.

ES6から使えるようになったPromise。
ブラウザによってはもう普通に使えたりします。

Promiseで非同期処理して、その値で同期処理するときはいいですが、
非同期処理をして、その値でまた非同期処理したい!っことあります。
そんなとき、then関数内でPromise返したりすることがありますが、
どう動くんだっけといまさら不安になったので確かめてみました。
ついでにネストしたらどうなるのかいろいろ試してみました。

準備

Chrome(Win, 49.0.2623.75m)のコンソールで実行しています。
記法はES6なので、BabelのplaygroundでES5にして試しています。
なお、Promiseは変換してもPolyfillに変わるのでなくそのままです。

まずは非同期関数を定義。

var asyncFunc = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('async!');
    }, 1000);
  });
};

値を返す関数

まずは普通の同期関数で試してみます。

var syncFunc = (value) => {
  return `${value} -> sync!`;
}
asyncFunc().then(syncFunc).then(value => {
  console.log(`${value} -> done!`);
});
// async! -> sync! -> done!

いたって普通ですね。
thenの関数内で値を返すと、次のthenの引数に値が渡ります。

Promiseを返す関数

さて、thenでPromiseを返すとどうなるでしょう?

var anotherAsyncFunc = (value) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`${value} -> anotherAsync!`);
    }, 1000);
  });
};
asyncFunc().then(anotherAsyncFunc).then(value => {
  console.log(`${value} -> done!`);
});
// async! -> anotherAsync! -> done!

ちゃんと実行されましたね。
returnされた値がPromiseだったら、
それをチェインするんじゃなく結果を待ってくれます。

Promiseを含むオブジェクトを返す関数

念のため、PromiseそのものでなくPromiseを含むオブジェクトの場合どうなるか見てみます。

var promiseObjectAsyncFunc = (value) => {
  return {
    p: new Promise(resolve => {
      setTimeout(() => {
        resolve(`${value} -> promiseObjectAsync!`);
      }, 1000);
    })
  };
};
asyncFunc().then(promiseObjectAsyncFunc).then(value => {
  console.log(`${value} -> done!`);
});
// [object Object] -> done!

ふむ。
案の定オブジェクトのまま返ってきているようです。
取り出してみます。

asyncFunc().then(promiseObjectAsyncFunc).then(value => {
  value.p.then(pValue => {
    console.log(`${pValue} -> done!`);
  });
});
// async! -> promiseObjectAsync! -> done!

普通に取り出せますね。やはり、Promiseそのものを返さないと待ってくれないようです。

解決しないPromiseを返す関数

resolveするのをうっかり忘れたらどうなるでしょう。

var noResolveAsyncFunc = (value) => {
  return new Promise(resolve => {});
};
asyncFunc().then(noResolveAsyncFunc).then(value => {
  console.log(`${value} -> done!`);
});
// (出力されない)

予想通り、何も出力されません。
Promiseを返す場合は、返り値Promiseが止まってないか注意する必要があります。

Promiseを返すPromiseを返す関数

試しにネストしてみたら...?

var nestedAsyncFunc = (value) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(new Promise(innerResolve => {
        setTimeout(() => {
          innerResolve(`${value} -> nestedAsync!`);
        }, 1000);
      }));
    }, 1000);
  });
};
asyncFunc().then(nestedAsyncFunc).then(value => {
  console.log(`${value} -> done!`);
});
// async! -> nestedAsync! -> done!

はい、普通に実行されます。再帰的に待ってくれてるようです。
でも実際これを利用することってあるんですかね...
チェインが止まったとき、どこでコケてるかを調査するのが面倒そうです。

ちなみに、内側のPromiseでうっかり外側のresolveを呼んでしまうと、
内側のresolve(innerResolve)を呼んでいないことになるので止まってしまいます。

var badNestedAsyncFunc = (value) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(new Promise(innerResolve => {
        setTimeout(() => {
          resolve(`${value} -> nestedAsync!`); // ここでうっかり外側resolve呼ぶ
        }, 1000);
      }));
    }, 1000);
  });
};
asyncFunc().then(badNestedAsyncFunc).then(value => {
  console.log(`${value} -> done!`);
}).catch(value => {
  console.log(`${value} -> fail!`);
});
// (出力されない)

これもわかりにくいバグの原因になりそうです。

失敗を返すPromiseを成功として返すPromiseを返す関数

だんだんややこしくなりますが、
さっきの例でネストした内部のPromiseが失敗を返したらどうなるんでしょう。

var innerRejectNestedAsyncFunc = (value) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(new Promise((innerResolve, innerReject) => {
        setTimeout(() => {
          innerReject(`${value} -> innerRejectNestedAsync!`);
        }, 1000);
      }));
    }, 1000);
  });
};
asyncFunc().then(innerRejectNestedAsyncFunc).then(value => {
  console.log(`${value} -> done!`);
}).catch(value => {
  console.log(`${value} -> fail!`);
});
// async! -> innerRejectNestedAsync! -> fail!

再帰的に待っていることから予想はしていましたが、失敗として発火しています。
resolveしてんのに失敗かよ!って思わないでもないです。

ということは、
「Promiseが失敗するのでreject周りを調べても原因がわからなかったが、
実はresolveで返すPromise内部でrejectしてました」、ってこともありえるわけです。
うーん、やっぱりネストは利点より欠点のほうが大きいような。

成功を返すPromiseを失敗として返すPromiseを返す関数

逆パターンいってみましょ。

var outerRejectNestedAsyncFunc = (value) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Promise(innerResolve => {
        setTimeout(() => {
          innerResolve(`${value} -> outerRejectNestedAsync!`);
        }, 1000);
      }));
    }, 1000);
  });
};
asyncFunc().then(outerRejectNestedAsyncFunc).then(value => {
  console.log(`${value} -> done!`);
}).catch(value => {
  console.log(`${value} -> fail!`);
});
// [object Promise] -> fail!

おや?

asyncFunc().then(outerRejectNestedAsyncFunc).then(value => {
  console.log(`${value} -> done!`);
}).catch(value => {
  value.then(innerValue => {
    console.log(`${innerValue} -> faildone!`);
  });
});

// async! -> outerRejectNestedAsync! -> faildone!

失敗したときはそれ以上のPromiseを展開せずに、そのままPromiseオブジェクトが返るようです。
考えてみりゃそりゃそうで、失敗したなら待っても意味無いですね。
むしろこのPromiseが成功したときに成功発火されても困ります。

失敗をキャッチしつつ返すPromiseを成功として返すPromiseを返す関数

もう名前がイミフですが、
2つ前の、「失敗を返すPromiseを成功として返すPromiseを返す関数」で
「値を返すcatch」を書いていたらどうなるでしょう。

var innerRejectInnerCatchNestedAsyncFunc = (value) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(new Promise((innerResolve, innerReject) => {
        setTimeout(() => {
          innerReject(`${value} -> innerRejectInnerCatchNestedAsync!`);
        }, 1000);
      }).catch(e => {
        return `${e} -> error!`;
      }));
    }, 1000);
  });
};
asyncFunc().then(innerRejectInnerCatchNestedAsyncFunc).then(value => {
  console.log(`${value} -> done!`);
}).catch(value => {
  console.log(`${value} -> fail!`);
});
// async! -> innerRejectOuterCatchNestedAsync! -> error! -> done!

はい、成功発火します。
そもそもcatch内で値を返すと以後その値でチェーンが継続するので、
「失敗をキャッチしつつ返すPromise」の時点で成功発火となります。

失敗を返すPromiseを成功として返すPromiseをキャッチしつつ返す関数

今度は外側でキャッチしてみます。

var innerRejectOuterCatchNestedAsyncFunc = (value) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(new Promise((innerResolve, innerReject) => {
        setTimeout(() => {
          innerReject(`${value} -> innerRejectOuterCatchNestedAsync!`);
        }, 1000);
      }));
    }, 1000);
  }).catch(e => {
    return `${e} -> error!`;
  });
};
asyncFunc().then(innerRejectOuterCatchNestedAsyncFunc).then(value => {
  console.log(`${value} -> done!`);
}).catch(value => {
  console.log(`${value} -> fail!`);
});
// async! -> innerRejectOuterCatchNestedAsync! -> error! -> done!

Promiseのネストを普通にキャッチしてるのとほぼ同じなので、成功発火します。

内側のPromiseのエラーをキャッチして再スローするつもりで値返したら
成功発火してしまってえらいことに、って失敗例が目に浮かびます。

ん、待てよ...?

失敗を返すPromiseを成功として返すPromiseをキャッチして失敗として返すPromiseを返す関数

ああ長い。さっきのでcatch内でPromiseを返せばどうなるでしょう。

var innerRejectOuterCatchAndRejectNestedAsyncFunc = (value) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(new Promise((innerResolve, innerReject) => {
        setTimeout(() => {
          innerReject(`${value} -> innerRejectOuterCatchAndRejectNestedAsync!`);
        }, 1000);
      });
    }, 1000);
  }).catch(e => {
    return new Promise((catchResolve, catchReject) => {
      catchReject(`${e} -> error!`);
    });
  }));
};
asyncFunc().then(innerRejectOuterCatchAndRejectNestedAsyncFunc).then(value => {
  console.log(`${value} -> done!`);
}).catch(value => {
  console.log(`${value} -> fail!`);
});
// async! -> innerRejectOuterCatchAndRejectNestedAsync! -> error! -> fail!

できた!catch内で「rejectを発火するPromise」を返せばエラーの再スローができます。
これでいくらでもネストできますね!

まとめ

  • then内の関数でPromiseを返すと再帰的にresolve実行を待ってくれる
  • rejectされた時点でそれ以上待たない
  • でもPromiseのネストはややこしいので用法用量を守って正しく使おう
  • というより使うな

追記: コメントでご指摘ありましたが、みんなasync/await使おうね!