Promiseと仲良くなって気持ち良く非同期処理を書こう

More than 1 year has passed since last update.

コールバック地獄を解消するPromiseパターン

Promiseは非同期処理のデザインパターンの一つです。非同期処理関数の戻り値として「処理の途中」を意味するオブジェクトを返す事で、かゆいところに手を届くようにします。ちなみに言語によってはFutureと呼ばれることもあります。(参考: future - Wikipedia
言語によって何が利点になるかは微妙に異なりますが、JavaScriptではコールバック地獄を抑止できる事が大きいでしょう。

JavaScriptでよく見られる非同期処理は、処理結果をコールバック関数で受けるパターンですね。標準APIでも頻繁に用いられています。

標準APIでの例
setTimeout(function(){
    // タイムアウト後に行う処理を書く
}, 1000);
自作関数での例
function myAsyncProcess(callback) {
    // 処理
    //
    otherAsyncProcess(function() { // 他の非同期関数を叩いたりする
        // 処理
        //
        callback(result); // 処理結果を引数にcallback関数を叩く
    });
}

// 利用側
myAsyncProcess(function(result) {
    // resultを使った処理
});

関数を変数として扱えるJavaScriptならではのシンプルな手法ですね。しかし、非同期処理を連続で使うような場面ではどうなってしまうでしょう?

コールバック地獄
myAsyncProcess1(function(result1) {
    // 処理
    //
    myAsyncProcess2(function(result2) {
        // 処理
        //
        // この辺で例外とか起きたらどうしよう…
        //
        myAsyncProcess3(function(result3) {
            // 処理
            //
        });
    });
});

なんということでしょう。シンプルかと思われた処理は避ける事の出来ないカオスへと変貌してしまったではありませんか。主にインデントがきもい。

Promiseパターンでは、以下のように書き表します。

Promiseを用いた場合
myAsyncProcess1()
    .then(function(result1) {
        // 処理
        //
        return myAsyncProcess2();
    }).then(function(result2) {
        // 処理
        //
        return myAsyncProcess3();
    }).then(function(result3) {
        // 処理
        //
    });

どうですか?インデントがなくなってスッキリすると思います。
そうでもないと感じる場合、おそらくreturnとthenというキーワードが増えたからでしょう。しかし、それさえ慣れてしまえばよいのです。

尚、この時のmyAsyncProcess1〜3()やthen()の戻り値のオブジェクトの事をPromiseオブジェクトと呼んだり、then()メソッドを持つ事からThenableオブジェクトと呼びます。

Promiseオブジェクトを今すぐ使い始める

必要な環境

EcmaScript6ではPromiseパターンに沿った非同期関数を簡単に実装するためのPromiseクラスが追加されます。
既にモダンブラウザではPromiseクラスが利用可能ですし、node.jsでも0.12から標準で利用可能になる予定です。
Promise - JavaScript | MDN

また、ライブラリも公開されています。Promiseのライブラリは複数公開されていて、主なものとして以下があります。

jakearchibald/es6-promise
仕様に沿った機能のみのシンプルな実装

petkaantonov/bluebird
node標準APIをPromise化するなどを持った強力な機能を持った実装

基本的な使い方

Promiseクラスを用いて、Promise版setTimeout()を作るとこうなります。

Promise版setTimeout()
function setTimeoutAsync(delay) {
    // Promiseクラスのインスタンスを関数の戻り値にする
    // Promiseクラスのコンストラクタの引数には関数を渡す
    // その関数は、resolve関数とreject関数を引数に取り、戻り値は無し
    return new Promise(function(resolve, reject) {
        // 非同期処理の完了コールバックとしてresolve関数を渡す
        setTimeout(resolve, delay);

        // または、以下のように完了コールバック内でresolve関数を呼び出してもOK
        // setTimeout(function() {
        //     resolve();
        // }, 1000);
    });
}

// 使い方
setTimeoutAsync(1000)
    .then(function() {
        // 処理
    });

エラーの処理

Promiseオブジェクトはエラー発生時の統一的な解決方法を持っています。
Promiseクラスのコンストラクタに渡す関数が、reject関数を受けていますが、非同期処理中にはresolve関数の代わりにこの関数を呼び出します。
そして、.then()と同様に.catch()を用いる事で、エラーをキャッチできます。

errors
function myAsyncProcess() {
    return thirdPartyFunc(function(err) {
        if (err) {
            // エラー発生時はreject関数を呼ぶ。
            // 引数としてエラー原因を渡す事が出来る。
            reject(err);
            return;
        }
        // 処理

        // 正常時はresolve関数を呼ぶ。
        // 引数として処理結果を渡す事が出来る。
        resolve(resultObject);
    });
}

// 使い方
myAsyncProcess()
    .then(function(resultObject) {
        // 正常時の処理
    })
    .catch(function(reason) {
        // 失敗時の処理
    });

Promiseの連結

then関数は何度でもthen関数をチェーンすることができ、それぞれ前のthen関数の処理が終わったときに呼び出されます。
then関数の戻り値に何か値を返すと、次のthen関数の引数になります。
そして、戻り値にPromiseオブジェクトを返すと、そのPromiseの完了を待ってから、次のthen関数を呼び出します。

chain
myAsyncProcess()
    .then(function(result) {
        // 処理

        return 100;
    })
    .then(anotherAsyncProcess) // Promiseを返す非同期関数をそのまま渡すことも可能
    .then(function(result) {
        // 処理

        if (error) {
            // Promise.reject()関数を使うと、失敗を表すPromiseオブジェクトを生成できる
            return Promise.reject('something error');
        }
        return nextAsyncProcess();
    })
    .then(function() {
        console.log('all done!');
    })
    .catch(function(reason) {
        // 
    });

この通り、Promiseは何度非同期処理が重なってもコールバック地獄に陥ることが無く、また、非同期処理を統一的に表すことが出来るインターフェイスです。
さあ、あなたのコードでもPromiseを使ってみませんか?

Promiseに関するページ

JavaScript Promiseの本
Promiseに関するもっと濃ゆい記事1

JavaScript Promises: There and back again - HTML5 Rocks
Promiseに関するもっと濃ゆい記事2

Promiseアンチパターン - くじら公園
本格的にPromiseを使うときに気を付けたいこと

非同期処理を気持ち良くする他のアプローチ

TypeScript - コールバック……駆逐してやる…この世から…一匹…残らず!! - Qiita
jQuery.Deferredやその他いろいろなライブラリでのコールバック駆逐について

JavaScriptの非同期処理には何を使うべきか - Qiita
Promiseや他の手段についてC#との比較を交えた解説

[JavaScript] 非同期処理のコールバック地獄から抜け出す方法 - Qiita
EcmaScript6のyieldを使って、非同期処理をもっと綺麗に書けるようにするcoライブラリについて