Skip to content

Latest commit

 

History

History
235 lines (173 loc) · 10.9 KB

promise-done.adoc

File metadata and controls

235 lines (173 loc) · 10.9 KB

Promise.prototype.done とは何か?

既存のPromise実装ライブラリを利用したことがある人は、 then の代わりに使う done というメソッドを見たことがあるかもしれません。

それらのライブラリでは Promise.prototype.done というような実装が存在し、 使い方は then と同じですが、promiseオブジェクトを返さないようになっています。

Promise.prototype.done は、ES PromisesPromises/A+の仕様には 存在していない記述ですが、多くのライブラリが実装しています。

このセクションでは、Promise.prototype.done とは何か? またなぜこのようなメソッドが多くのライブラリで実装されているかについて学んでいきましょう。

doneを使ったコード例

実際にdoneを使ったコードを見てみると done の挙動が分かりやすいと思います。

promise-done-example.js
link:embed/embed-promise-done-example.js[role=include]
// => ブラウザの開発ツールのコンソールを開いてみましょう

最初に述べたように、Promise.prototype.done は仕様としては存在しないため、 利用する際は実装されているライブラリを使うか自分で実装する必要があります。

実装については後で解説しますが、まずは then を使った場合と done を使ったものを比較してみます。

thenを使った場合
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オブジェクトを返す関数を考えてみましょう。

json-promise.js
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 していれば何も問題がないのですが、その処理を忘れてしまうというミスを した時にどこでエラーが発生してるのかわからなくなるというヒューマンエラーを助長させる面があります。

catchによるエラーハンドリングを忘れてしまった場合
const string = "jsonではない文字列";
JSONPromise(string).then((object) => {
    console.log(object);
}); // (1)
  1. 例外が投げられても何も処理されない

JSON.parse のような分かりやすい例の場合はまだよいですが、 メソッドをtypoしたことによるSyntax Errorなどはより深刻な問題となりやすいです。

typoによるエラー
const string = "{}";
JSONPromise(string).then((object) => {
    conosle.log(object);// (1)
});
  1. conosle というtypoがある

この場合は、consoleconosle とtypoしているため、以下のようなエラーが発生するはずです。

ReferenceError: conosle is not defined

しかし、Promiseではtry-catchされるため、エラーが握りつぶされてしまうという現象が起きてしまいます。 毎回、正しく catch の処理を書くことができる場合は何も問題ありませんが、 Promiseの実装によってはこのようなミスが検知しにくくなるケースがあることを知っておくべきでしょう。

このようなエラーの握りつぶしはunhandled rejectionと言われることがあります。 "Rejectedされた時の処理がない"というそのままの意味ですね。

Note

このunhandled rejectionが検知しにくい問題はPromiseの実装と実行環境に依存します。

たとえば、 Bluebird では、 明らかに人間のミスにみえるReferenceErrorの場合などをコンソールにエラーとして表示してくれます。

"Possibly unhandled ReferenceError. conosle is not defined

また、このunhandled rejectionに関する仕組みが ECMAScript 2016 で仕様に追加されています。そのためネイティブPromiseでは、この仕様を活用したGC-based unhandled rejection trackingというものが搭載されているケースが増えています。

これはpromiseオブジェクトがガーベッジコレクションによって回収されるときに、 それがunhandled rejectionであるなら、エラー表示をするという仕組みがベースになっています。

FirefoxChrome のネイティブPromiseでは一部実装されています。

doneの実装

Promiseにおける done は先程のエラーの握りつぶしを避けるにはどうするかという方法論として、 そもそもエラーハンドリングをしなければいい という豪快な解決方法を提供するメソッドです。

done はPromiseの上に実装することができるので、 Promise.prototype.done というPromiseのprototype拡張として実装してみましょう。

promise-prototype-done.js
link:lib/promise-prototype-done.js[role=include]

どのようにPromiseの外へ例外を投げているかというと、 setTimeoutの中でthrowをすることで、外へそのまま例外を投げられることを利用しています。

setTimeoutのコールバック内での例外
try {
    setTimeout(() => {
        throw new Error("error");// (1)
    }, 0);
} catch (error) {
    console.error(error);
}
  1. この例外はキャッチされない

Note

なぜ非同期の callback 内での例外をキャッチ出来ないのかは以下が参考になります。

Promise.prototype.done をよく見てみると、何も return していないことも分かると思います。 つまり、done は「ここでPromise chainは終了して、例外が起きた場合はそのままpromiseの外へ投げ直す」という処理になっています。

現在では多くの実行環境で、unhandled rejectionを検知してコンソールに警告を表示するため、done が必要な場合は少なくなっています。 また今回のPromise.prototype.doneのように、done は既存のPromiseの上に実装することができるため、 ES Promisesの仕様そのものには入らなかったといえるかもしれません。

Note
今回の Promise.prototype.done の実装は promisejs.org を参考にしています。

まとめ

このセクションでは、 QBluebirdprfun 等 多くの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の拡張を書く手法について書かれています。

  • Promise.prototype を拡張する方法

  • Wrapper/Delegate を使った抽象レイヤーを作る方法

また、Delegateを利用した方法については、 Chapter 28. Subclassing Built-ins にて 詳しく解説されています。