LoginSignup
39
40

More than 5 years have passed since last update.

ES6-Promiseをネストしてみる

Last updated at Posted at 2016-03-04

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使おうね!

39
40
3

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
39
40