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