Programming Field

ES2015のPromise

スポンサーリンク

JavaScriptにおける「Promise」は、現代の「JavaScript」の仕様元になっているECMAScript 2015(通称「ES2015」など)以降に含まれる型(クラス)です。Promiseは「非同期」処理とともによく用いられ、その処理が「完了」したとき、あるいは「失敗」したときに指定されたハンドラーの処理を実行します。

※ ES2015で新たに導入された型であるため、IE11など、ES2015に準拠していないブラウザー/スクリプトエンジンでは利用できません。ただし、多くのPolyfill実装(実際の Promise を模した実装)があるため、「core-js」「es6-promise」などのPolyfill実装を提供するライブラリを組み込むことで、ほぼ同等の処理を利用することができます。

[関連キーワード: Promise]

単純な例

console.log('creating promise');
var p = new Promise(function (resolve, reject) {
    console.log('promise executor');
    setTimeout(function () {
        console.log('setTimeout callback');
        resolve(35);
    }, 2000);
});
console.log('promise created');
p.then(function (val) {
    console.log('resolved: ' + val);
});
console.log('Promise.prototype.then called');

コンソールログ出力例:

creating promise
promise executor
promise created
Promise.prototype.then called
(2秒後)
setTimeout callback
resolved: 35

Promiseの処理の流れ

Promiseオブジェクトの生成

Promiseのオブジェクトは主に以下のような方法で取得されます。

  1. new Promise(...) で明示的に作成する
  2. Promise.resolve(...) など、Promise 直下にある一部のメソッド(スタティック関数)を利用する
  3. window.fetch 関数などの組み込み関数、または各種ライブラリが提供する関数/メソッドが返す
  4. [ES2017 以降] async function を普通に呼び出した時の戻り値として得る

基本は1番目の処理であり、3番目・4番目は内部的に1番目と近しい処理を行っています(2番目も1番目を簡易的にしたものと考えることもできます)。1番目の処理は「Promiseコンストラクター」を呼び出す処理であり、冒頭の例のようにコールバック関数(executorと呼ばれます)を渡すことで遅延可能な処理の実行を行うことができます。

// Promiseコンストラクターを呼び出してオブジェクトを生成
var p = new Promise(function (resolve, reject) {
    // この中の処理はすぐに実行されるが、resolve または reject を呼び出すまで「処理中」という扱いにできる
});

※ あくまで「遅延可能」と考える必要があり、executorはコンストラクター呼び出し時点で即時実行されます。

そして、生成されたPromiseオブジェクトには主に「then」と「catch」の2つのメソッド(Promise.prototype.then / Promise.prototype.catch)を介してコールバック関数を渡し、それぞれ成功(resolve)または失敗(reject)したときに遅延して行うべき処理を指定することができます(= resolve または reject したときに処理が実行されます)。

// Promise内の処理(executor)が resolve したときに遅延して呼び出されるコールバックを指定
p.then(function (val) {
    console.log('resolved: ' + val);
});
// Promise内の処理が reject したときに遅延して呼び出されるコールバックを指定
p.catch(function (val) {
    console.log('rejected: ' + val);
});

※ catch メソッドを使う代わりに、then メソッドの2番目の引数に reject された場合の処理を指定することができます。(.catch(onRejected) の呼び出しは .then(undefined, onRejected) と等価です。)
※ かなり古いブラウザー/スクリプトエンジンでは「catch」が予約語のためにメソッド名として使用できない場合があります。その場合は p["catch"](...) のように呼び出します。
※ executor 内で catch されない例外がスローされた場合(throw ステートメントによるものを含む)は、スローされたエラー/値をパラメーターとして reject が呼び出された扱いとなります。ただし executor の処理を抜けた後に発生した場合は単純な未キャッチ例外となります。

なお、Promiseオブジェクトが生成された時点でexecutorの処理が完了し、(executor の第1・第2引数に入る) resolve または reject が呼び出されている場合でも、then および catch メソッドに渡したコールバックは即時実行されません。既に実行が完了している場合、および resolve または reject が遅延して呼び出された場合、いずれの場合でも、then および catch に指定されたコールバックの実行はスクリプトエンジンのジョブキューに積まれ、エンジン側に制御が返ってから呼び出されることになります。そのため、実行順序は以下のようになります。

console.log('1'); // 1番目
var p = new Promise(function (resolve, reject) {
    console.log('2'); // 2番目
    resolve();
});
console.log('3'); // 3番目
p.then(function () {
    console.log('5'); // 5番目
});
console.log('4'); // 4番目

※ キューに積まれたジョブの実行順序やタイミングは規定がありますが、キューにはPromise由来に限らない様々なものが積まれる可能性があり、他のジョブがいつどのくらい積まれるかは処理次第であるため、このタイミングを制御・タイミングに依存するような処理は避ける必要があります。

ハンドル処理のチェーン

[関連キーワード: Promise.prototype.then, Promise.prototype.catch]

Promiseオブジェクトの then メソッドおよび catch メソッドは、それぞれ新たなPromiseオブジェクトを生成します。このオブジェクトは、それぞれのメソッドに渡されたコールバック関数の処理が以下のように返す戻り値に応じて、resolve または reject される(扱いとなる)Promiseオブジェクトとなります。

  1. コールバック関数がPromiseオブジェクトを返す
    → そのオブジェクトが resolve されたら resolve、reject されたら reject される(新たな)Promiseオブジェクト
  2. コールバック関数が上記以外の値を返す、または何も返さない(undefined 値を返す扱いになる)
    → 返された値(undefined を含む)を引数に resolve が呼び出されたものとする(新たな)Promiseオブジェクト
  3. コールバック関数が例外をスローする
    → スローされた値を引数に reject が呼び出されたものとする(新たな)Promiseオブジェクト

※ 1点目の「Promiseオブジェクト」は、厳密には「then メソッド(関数)を持つオブジェクト」であり、そのオブジェクトに対する .then(onResolved, onRejected) といった呼び出しが行われます。これを thenable なオブジェクトと呼ぶことがあります(TypeScriptでは PromiseLike 型を指します)。これにより、スクリプトエンジンによる実装ではない(いわゆるPolyfillなどの)Promiseオブジェクトを混ぜて使用することができます。
※ 上記の処理は、厳密には Promise.resolve 関数の内部処理と同じ処理として行われるものとなります。
※ catch メソッド、または then メソッドの2番目に指定されたコールバックであっても、それが実行された場合に上記で reject に該当するケースとならなかった場合は、catch (または then) が返すPromiseオブジェクトは resolve されるPromiseオブジェクトとなります。

これを利用すると、Promiseのチェーン(chain; 連鎖)を作ることができます。

console.log('1'); // 1番目
var p = new Promise(function (resolve, reject) {
    console.log('2'); // 2番目
    resolve();
});
console.log('3'); // 3番目
p.then(function () {
    console.log('5'); // 5番目
    return 'Hello';
}).then(function (msg) { // 直前のPromiseが新たな値でresolveしているのでその内容が受け取れる
    console.log('6'); // 6番目
    // 「thenable」なオブジェクトを返す
    return {
        then: function (onFulfill) {
            // onFulfill を呼び出すことでresolveな状態に持っていく
            onFulfill(msg + ', world');
        }
    };
}).then(function (msg) { // 直前のPromiseが新たな値でresolve相当の処理をしているのでその内容が受け取れる
    console.log('7'); // 7番目
    console.log(msg); // 「Hello, world」
    return Promise.reject(new Error('xxxxx'));
}).then(function (msg) {
    // 直前のPromiseがrejectされる(はず)なのでここは呼び出されない
    console.log('never1');
    console.log(msg);
}).catch(function (err) {
    console.log('8'); // 8番目
    console.log(err); // Errorオブジェクト(メッセージ=「xxxxx」)
}).catch(function (err) {
    // 直前のPromiseがresolveされる(はず)なのでここは呼び出されない
    // ※ 直前のcatchの中でrejectしていない
    console.log('never2');
    console.log(err);
});
console.log('4'); // 4番目

※ 前述の通り、Promiseチェーンを使う場合、チェーンの中で catch が行われると、その catch の中で新たな reject が起きない限り後続の catch の処理(onRejected)は呼び出されません。「catch した」=「適切にエラーハンドリングをした(のでこれ以上エラーハンドリングする必要が無い)」と考える必要があり、単にエラー情報を変換するだけ、あるいは既知のエラーのみハンドリングしたいなど、後続の処理にさらなるエラーハンドリングを要求したい場合は、catch の中で reject を行う必要があります。

チェーンを使うと、以下のようにPromiseの結果(resolve や reject に渡された値)を変換して扱いやすくすることができます。

// 指定されたURLからXMLを取得し、resolve の結果がパースされたDocumentとなるPromiseオブジェクトを返す
// (取得できなかったかパースに失敗した場合は少なくとも「type」と「detail」のフィールドを持つオブジェクトとともに reject される)
function fetchXML(url) {
    // fetch APIで読み込みを行う
    return fetch(url)
        // 取得に成功したら、取得出来たデータをテキストデータとして得る
        .then(function (response) {
            // text メソッドはテキストデータで resolve されるPromiseオブジェクトを返す
            return response.text();
        })
        // ここまででエラーが起きた場合、それを取得失敗とわかるようなオブジェクトで reject しなおす
        .catch(function (err) {
            return Promise.reject({
                type: 'fetch_error',
                detail: err
            });
        })
        // テキストデータが得られたらDocumentにパースする
        .then(function (text) {
            try {
                var parser = new DOMParser();
                // ※ エラーが発生した場合に例外が発生するかどうかはブラウザーによって変わります。
                return parser.parseFromString(text, 'application/xml');
            } catch (err) {
                // 例外が発生したらパースで発生したことを分かるようなオブジェクトを使って reject する
                return Promise.reject({
                    type: 'parse_error',
                    data: text,
                    detail: err
                });
            }
        });
}

// 以下のように使うことができる
fetchXML('/data.xml').then(function (doc) {
    console.log(doc.documentElement.tagName);
}).catch(function (err) {
    console.error('Error: type = ', err.type, ', detail = ', err.detail);
});

Promise.prototype.finally

※ この内容はES2018に含まれる内容であり、一部のブラウザー/スクリプトエンジンでは利用できません。

Promiseオブジェクトのfinallyメソッドは、Promiseオブジェクトが resolve または reject となったときに呼び出されるコールバックを指定できます。そのコールバック自身は値を受け取らず、またコールバックの戻り値は無視され、finally 自身の戻り値となるPromiseオブジェクトはその前の(チェーンされた)Promiseオブジェクトにおける resolve / reject の内容になります。これを利用することで、resolve・reject にかかわらず後処理的な内容をPromiseチェーンの中に記述することができます。

finally をPromiseのチェーンに含めた場合、呼び出し順序は以下のようになります。

console.log('1'); // 1番目
var p = new Promise(function (resolve, reject) {
    console.log('2'); // 2番目
    resolve();
});
console.log('3'); // 3番目
p.then(function () {
    console.log('5'); // 5番目
    return 'Hello';
}).finally(function () { // 直前のPromiseがresolveになったタイミングで実行される
    // (※ finally 内のコールバック関数では何も引数を受け取ることができない)
    console.log('6'); // 6番目
}).then(function (msg) { // 直前のfinally処理はresolve/rejectの内容をそのまま次に渡すので、さらに前のPromiseによる内容が受け取れる
    console.log('7'); // 7番目
    console.log(msg); // 「Hello」
    return Promise.reject(new Error('xxxxx'));
}).finally(function () { // rejectの場合でもそのタイミングで実行される
    console.log('8'); // 8番目
}).then(function (msg) {
    // finally直前のPromiseがrejectされる(はず)なのでここは呼び出されない
    console.log('never1');
    console.log(msg);
}).catch(function (err) {
    console.log('9'); // 9番目
    console.log(err); // Errorオブジェクト(メッセージ=「xxxxx」)
});
console.log('4'); // 4番目

※ async function 内での try...catch ステートメントの finally 句に似ていますが、Promise.prototype.finally メソッドはチェーンの途中に置くことができる点、および後続のチェーンの処理を断つことができない点で違いがあります。

スポンサーリンク

Promiseの便利関数

「Promise」コンストラクターはオブジェクトとして、いくつかのメソッド(スタティック関数)を提供しています。

Promise.resolve

Promise.resolve メソッドは、引数を1つ受け取り、原則として「resolve な状態」となるPromiseオブジェクトを生成します。

// Promise.resolveを呼び出してオブジェクトを生成
Promise.resolve(12345).then(function (value) {
    // この中では「value」として「12345」を受け取れる
});

また、Promise.resolve メソッドの特徴として、第1引数がthenableなオブジェクト(then メソッドを持つオブジェクト)である場合、そのthenメソッドを呼び出すことでresolveな状態、またはrejectな状態となるPromiseオブジェクトを生成します。

// Promise.resolveの第1引数をthenableとする
Promise.resolve({
    then: function (onFulfill, onReject) {
        if (window.confirm('OK?')) {
            onFulfill();
        } else {
            onReject(null);
        }
    }
}).then(function () {
    // confirmダイアログでOKが行われたときの処理
}, function (err) {
    // confirmダイアログでキャンセルが行われた、またはエラーが発生したときの処理
});

※ 当然ながら、Promiseオブジェクトもthenableなオブジェクトの一種であるため、Promiseオブジェクトを Promise.resolve に渡した場合は、そのオブジェクトの resolve / reject な状態を利用します。
※ 上記は例のため簡略化していますが、resolveの場合と異なりrejectの場合は必ずしも決まった型のオブジェクトにはならない可能性があります。具体的には、前述のチェーン処理のように then の処理が複数存在し、その中のいずれかで実行時エラー(ハンドルされない例外)が発生した場合、その理由を表すエラーオブジェクトともにreject扱いとなります。チェーンが複雑化する場合は何の理由でrejectとなったか利用者が把握しにくくなるため、reject時は常にエラーオブジェクトを使うか、適宜チェーンの中でcatchを行って改めてrejectしなおすなどの対応を行うのが親切です。

以上の特徴から、「Promiseオブジェクトかどうかわからないデータがあるがそのデータでresolveしたい」という状況では、とりあえず Promise.resolve を呼び出しておく、という方法が有用です。

// Promise.resolveを呼び出してオブジェクトを生成
Promise.resolve(SomeLibrary.execute()).then(function (result) {
    // 「SomeLibrary.execute」がPromiseオブジェクトを返すのであればそのresolveされた結果を、通常の値を返すのであればその値を得られる
});

※ この例では、「SomeLibrary.execute()」が(Promiseオブジェクトを返さずに)例外をスローする場合を適切にハンドルできていない点にご注意ください。

Promise.reject

Promise.reject メソッドは、引数を1つ受け取り、常に「reject な状態」となるPromiseオブジェクトを生成します。

// Promise.rejectを呼び出してオブジェクトを生成
Promise.reject(new Error('!?')).catch(function (err) {
    // この中では「err」として「!?」というメッセージを持つエラーオブジェクトを受け取れる
});

Promise.resolve と異なり、Promise.reject は受け取った引数が thenable かどうかで処理の分岐を行わず、その値をそのまま reject の理由を表す値として利用します。

Promise.all

Promise.all メソッドは、引数として「配列」(厳密にはiteratorをサポートするオブジェクト)を1つ受け取り、その値(配列など)が含むすべての要素で resolve となった場合に resolve となる Promise オブジェクトを返します。

// Promise.allを呼び出してオブジェクトを生成
Promise.all([SomeLibrary.execute(1), SomeLibrary.execute(2)]).then(function (value) {
    // この中では「value」として「SomeLibrary.execute(1)」と「SomeLibrary.execute(2)」の resolve 結果を配列で受け取れる
});

Promise.all が作成する Promise オブジェクトの resolve 結果は配列のオブジェクトとなり、引数として渡された配列(など)に含まれる要素の resolve 結果が、その要素順に格納されます。なお、Promise.all の各要素に対して Promise.resolve が呼び出されるため、各要素が thenable でない場合でもその値がそのまま resolve 結果として用いられます。

Promise.all は通信処理などスクリプトの外で待機する必要があるものが複数存在した場合に、それらを1つにまとめて一括で待機する際によく用いられます。

なお、いずれかの要素が1つでも reject となった場合、Promise.all の戻り値のオブジェクトは最も早く reject となったその理由とともに reject な状態となります。この場合どの要素が reject した結果かは(reject の理由を表す値に情報が含まれない限り)わかりませんのでご注意ください。reject のハンドリングに優先順位を付けたい場合は、以下のように各要素の Promise オブジェクトを先にすべて作成してから小分けに then 呼び出しを行うことができます。

// 上記のコード例を、Promise.allを使わずに、rejectに優先順位をつけて待機する場合の処理
// ※ 通信処理などを伴うPromiseオブジェクトが返る場合、そのトリガーは行いつつ完了を待たずにPromiseオブジェクトが得られるので
//    実質並列に通信処理などを待機することができる
var p1 = SomeLibrary.execute(1);
var p2 = SomeLibrary.execute(2);
var p3 = Promise.resolve(p2).then(function (value2) {
    // p2 の結果を受け取ってから p1 の結果受け取りを試みる
    // (p1 が reject になった場合はその内容をそのまま本コールバック関数の reject 内容とする)
    return Promise.resolve(p1).then(function (value1) {
        // 2つの結果がそろったのでそれをまとめる値を返す
        return [value1, value2];
    });
});

p3.then(function (value) {
    // この中では「value」として「[value1, value2]」の配列を受け取れる
}).catch(function (reason) {
    // p3 は p2 が then になってから p1 の resolve / reject を見るPromiseオブジェクトとなっているため、
    // p1, p2 いずれも reject となった場合は p2 の理由が優先される
    // ※ その場合、p1 の reject はスクリプトエンジンによっては「キャッチされない例外」扱いとなるため、
    //    回避する必要がある場合何も処理をしない関数を .catch メソッドに渡すなどが必要になる
});

Promise.race

Promise.race メソッドは、Promise.all メソッドと同様に引数として「配列」(iteratorをサポートするオブジェクト)を指定します。Promise.all と異なるのは、受け取った値の各要素がどれか1つでも resolve または reject になったときに、その内容をそのまま resolve または reject とするような Promise オブジェクトを返します。

// Promise.allを呼び出してオブジェクトを生成
Promise.race([SomeLibrary.execute(1), SomeLibrary.execute(2)]).then(function (value) {
    // 「value」の値は、「SomeLibrary.execute(1)」と「SomeLibrary.execute(2)」で先に resolve となった結果を受け取れる
    // (ただしどちらかが先に reject になった場合はここに到達しない)
}, function (reason) {
    // 「reason」の値は、「SomeLibrary.execute(1)」と「SomeLibrary.execute(2)」でどちらかが先に reject となった場合にその内容を受け取れる
    // (ただしどちらかが先に resolve になった場合はここに到達しない)
});

Promise.race は各要素のいずれかが resolve または reject になった場合に即座にその結果を用いるため、残りの要素の結果は無視されることになります。また、どの結果を用いるかは完全にスピード勝負になってしまうため、Promise.race の利用用途は比較的限定的になると思われます。考えられる用途としては、複数の並列に実行させる処理がほぼ同じような結果を返す場合や、結果が別途ハンドルされるものとして少なくとも1つの結果が得られたときに表示などを更新する場合などに利用できます。

最終更新日: 2018/08/04