Edited at

ES6-Promiseをネストしてみる

More than 3 years have 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使おうね!