既存のPromise実装ライブラリを利用したことがある人は、
then
の代わりに使う done
というメソッドを見たことがあるかもしれません。
それらのライブラリでは Promise.prototype.done
というような実装が存在し、
使い方は then
と同じですが、promiseオブジェクトを返さないようになっています。
Promise.prototype.done
は、ES PromisesやPromises/A+の仕様には
存在していない記述ですが、多くのライブラリが実装しています。
このセクションでは、Promise.prototype.done
とは何か?
またなぜこのようなメソッドが多くのライブラリで実装されているかについて学んでいきましょう。
実際にdoneを使ったコードを見てみると done
の挙動が分かりやすいと思います。
link:embed/embed-promise-done-example.js[role=include]
// => ブラウザの開発ツールのコンソールを開いてみましょう
最初に述べたように、Promise.prototype.done
は仕様としては存在しないため、
利用する際は実装されているライブラリを使うか自分で実装する必要があります。
実装については後で解説しますが、まずは then
を使った場合と done
を使ったものを比較してみます。
const promise = Promise.resolve();
promise.then(() => {
JSON.parse("this is not json");
}).catch((error) => {
console.error(error);// => "SyntaxError: JSON.parse"
});
比べて見ると以下のような違いがあることが分かります。
-
done
はpromiseオブジェクトを返さない-
つまり、doneの後に
catch
等のメソッドチェーンはできない
-
-
done
の中で発生したエラーはそのまま外に例外として投げられる-
つまり、Promiseによるエラーハンドリングが行われない
-
done
はpromiseオブジェクトを返していないので、
Promise chainの最後におくメソッドというのは分かると思います。
また、Promiseには強力なエラーハンドリング機能があると紹介していましたが、
done
の中ではそのエラーハンドリングをワザと突き抜けて例外を出すようになっています。
なぜこのようなPromiseの機能とは相反するメソッドが、多くのライブラリで実装されているかについては 次のようなPromiseの失敗例を見ていくと分かるかもしれません。
Promiseには強力なエラーハンドリング機能がありますが、 (デバッグツールが上手く働かない場合に) この機能がヒューマンエラーをより複雑なものにしてしまう一面があります。
これは、then or catch?でも同様の内容が出てきたことを覚えているかもしれません。
次のような、promiseオブジェクトを返す関数を考えてみましょう。
link:embed/embed-json-promise.js[role=include]
渡された値を JSON.parse
してpromiseオブジェクトを返す関数ですね。
以下のように使うことができ、JSON.parse
はパースに失敗すると例外を投げるので、
それを catch
することができます。
link:embed/embed-json-promise.js[role=include]
// 実行例
var string = "jsonではない文字列";
JSONPromise(string).then(function (object) {
console.log(object);
}).catch((error) => {
// => JSON.parseで例外が発生した時
console.error(error);
});
ちゃんと catch
していれば何も問題がないのですが、その処理を忘れてしまうというミスを
した時にどこでエラーが発生してるのかわからなくなるというヒューマンエラーを助長させる面があります。
const string = "jsonではない文字列";
JSONPromise(string).then((object) => {
console.log(object);
}); // (1)
-
例外が投げられても何も処理されない
JSON.parse
のような分かりやすい例の場合はまだよいですが、
メソッドをtypoしたことによるSyntax Errorなどはより深刻な問題となりやすいです。
const string = "{}";
JSONPromise(string).then((object) => {
conosle.log(object);// (1)
});
-
conosle というtypoがある
この場合は、console
を conosle
とtypoしているため、以下のようなエラーが発生するはずです。
ReferenceError: conosle is not defined
しかし、Promiseではtry-catchされるため、エラーが握りつぶされてしまうという現象が起きてしまいます。
毎回、正しく catch
の処理を書くことができる場合は何も問題ありませんが、
Promiseの実装によってはこのようなミスが検知しにくくなるケースがあることを知っておくべきでしょう。
このようなエラーの握りつぶしはunhandled rejectionと言われることがあります。 "Rejectedされた時の処理がない"というそのままの意味ですね。
Note
|
このunhandled rejectionが検知しにくい問題はPromiseの実装と実行環境に依存します。 たとえば、 Bluebird では、 明らかに人間のミスにみえるReferenceErrorの場合などをコンソールにエラーとして表示してくれます。
また、このunhandled rejectionに関する仕組みが ECMAScript 2016 で仕様に追加されています。そのためネイティブPromiseでは、この仕様を活用したGC-based unhandled rejection trackingというものが搭載されているケースが増えています。 これはpromiseオブジェクトがガーベッジコレクションによって回収されるときに、 それがunhandled rejectionであるなら、エラー表示をするという仕組みがベースになっています。 |
Promiseにおける done
は先程のエラーの握りつぶしを避けるにはどうするかという方法論として、
そもそもエラーハンドリングをしなければいい という豪快な解決方法を提供するメソッドです。
done
はPromiseの上に実装することができるので、
Promise.prototype.done
というPromiseのprototype拡張として実装してみましょう。
link:lib/promise-prototype-done.js[role=include]
どのようにPromiseの外へ例外を投げているかというと、 setTimeoutの中でthrowをすることで、外へそのまま例外を投げられることを利用しています。
try {
setTimeout(() => {
throw new Error("error");// (1)
}, 0);
} catch (error) {
console.error(error);
}
-
この例外はキャッチされない
Note
|
なぜ非同期の |
Promise.prototype.done
をよく見てみると、何も return
していないことも分かると思います。
つまり、done
は「ここでPromise chainは終了して、例外が起きた場合はそのままpromiseの外へ投げ直す」という処理になっています。
現在では多くの実行環境で、unhandled rejectionを検知してコンソールに警告を表示するため、done
が必要な場合は少なくなっています。
また今回のPromise.prototype.done
のように、done
は既存のPromiseの上に実装することができるため、
ES Promisesの仕様そのものには入らなかったといえるかもしれません。
Note
|
今回の Promise.prototype.done の実装は promisejs.org を参考にしています。
|
このセクションでは、 Q や Bluebird や prfun 等
多くのPromiseライブラリで実装されている done
の基礎的な実装と、then
とはどのような違いがあるかについて学びました。
done
には次の2つの側面があることがわかりました。
-
done
の中で起きたエラーは外へ例外として投げ直す -
Promise chain を終了するという宣言
then or catch? と同様にPromiseにより沈黙してしまったエラーについては、 デバッグツールやライブラリの改善で問題となるケースは少なくなっています。
また、done
は値を返さないことでそれ以上Promise chainを繋げることができなくなるため、
そのような統一感を持たせるという用途で done
を使うこともできます。
ES Promises では根本に用意されてる機能はあまり多くありません。 そのため、自ら拡張したり、拡張したライブラリ等を利用するケースが多いと思います。
そのときでも何でもやり過ぎると、せっかく非同期処理をPromiseでまとめても複雑化してしまう場合があるため、 統一感を持たせるというのは抽象的なオブジェクトであるPromiseにおいては大事な部分といえるかもしれません。
Note
|
Promises: The Extension Problem (part 4) | getiblog では、 Promiseの拡張を書く手法について書かれています。
また、Delegateを利用した方法については、 Chapter 28. Subclassing Built-ins にて 詳しく解説されています。 |