銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

「プロミス地獄」に落ちないための基本事項を書いておきます

「コールバック地獄」からボクらを救ってくれた「Promise」ですが、ふと気が付けば、ちょっと種類の違う別の地獄に落ちてる場合がありますよと。

「なんだPromiseお前もかっ!」的な(笑)

f:id:takamints:20170618182837p:plain

「コールバック地獄」は見た目にネストが深くて「ダメだコリャ感」がわかりやすい。 一方言わば「Promise地獄」は、パッと見スッキリしてるんだけど、少し複雑になると、ホントに正しく動いているのかどうか判別しにくい。 というのも、Promise的に間違っていてもJavaScriptの構文的には正しいことが多々あって、どこでバグっているかがわかりにくい。 結果、変なハマり方をしてしまうんですね。

てことで、今まで自分でハマった「Promise地獄」を思い返して知見をまとめておこうと思います。

約束 (創元推理文庫)
約束 (創元推理文庫)
posted with amazlet at 17.06.18
東京創元社 (2017-05-11)
売り上げランキング: 8,345

Promiseのハンドラーでtry~catchする必要はありません

Promiseオブジェクト生成時のexecutor関数内からエラーが投入されると、そのPromiseはrejectされます。

つまり特別な理由がない限りtry~catchでエラーを捕まえてrejectする必要はありません。

throw-error-from-ctor.js

function someAsync() {
    return new Promise(function(resolve, reject) {
        throw new Error("to reject the promise");
    });
}

someAsync().then(function() {
    console.error("OK.");
}).catch(function(err) {
    console.error("Error:", err.message);
});

実行結果

$ node throw-error-from-ctor.js
Error: to reject the promise

$ 

つまり以下は冗長です。実行結果は上と同じ。

throw-error-redundant.js

function someAsync() {
    return new Promise(function(resolve, reject) {
        try {
            throw new Error("to reject the promise.");
        } catch(err) {
            reject(err);
        }
    });
}

そしてコレはthenでも同じ

上のことはthenのonfulfilledにも当てはまります。つまりthenの中でtry~catchする必要はありません。 以下のPromiseもrejectされます。

throw-error-from-then.js

function someAsync() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { resolve(); }, 1000);
    });
}

someAsync().then(function() {
    throw new Error("to reject the promise");
}).catch(function(err) {
    console.error("Error:", err.message);
});

しかしネストした非同期処理はtry-catchで囲むべきです

以下のコードでも、Promiseは結果的にrejectされていますが、投げられたエラーがキャッチされておらず、スタックトレースがプリントされています。

function someAsync() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { resolve(); }, 1000);
    });
}

someAsync().then(function() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            throw new Error("to reject the promise");
        }, 1000);
    });
}).then(function() {
    console.error("OK.");
}).catch(function(err) {
    console.error("Error:", err.message);
});

実行結果

$ node throw-error-from-then-nest.js
throw-error-from-then-nest.js:10
            throw new Error("to reject the promise");
            ^

Error: to reject the promise
    at Timeout._onTimeout (throw-error-from-then-nest.js:10:19)
    at ontimeout (timers.js:365:14)
    at tryOnTimeout (timers.js:237:5)
    at Timer.listOnTimeout (timers.js:207:5)

実は予想外の動きでした。rejectされないと思っていたのです。もしかして古いバージョンではrejectされないかと確認してみましたが、少なくともnode 4.8.3ではrejectされていました。 なんにせよ、上のようにネストした非同期処理は、以下のようにtry~catchでエラーをハンドリングして明示的にrejectしたほうが良さそうです。

function someAsync() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { resolve(); }, 1000);
    });
}

someAsync().then(function() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            try {
                throw new Error("to reject the promise");
            } catch(err) {
                reject(err);
            }
        }, 1000);
    });
}).then(function() {
    console.error("OK.");
}).catch(function(err) {
    console.error("Error:", err.message);
});

そのnew Promise、ホントに必要?

Promiseチェーンが長くなってくると、その一部分をまとめてPromiseを返す関数として独立させたくなったりしますが、その新しい関数の中でPromiseオブジェクトを生成する必要は多分ありません。

以下のようにthenがフラットにたくさん続くと、それはそれでイラッときます。

someAsync().then(function () {
    return asyncA1();
}).then(function () {
    return asyncA2();
}).then(function () {
    return asyncA3();
}).then(function () {
    return asyncB1();
}).then(function () {
    return asyncB2();
}).then(function () {
    console.log("OK.");
}).catch(function(err) {
    console.error("Error:", err.message);
});

非同期処理をAとBでまとめるには、以下のように組み直せば良いのですが、、、

function asyncA() {
    return asyncA1().then(function() {
        return asyncA2();
    }).then(function () {
        return asyncA3();
    });
}

function asyncB() {
    return asyncB1().then(function() {
        return asyncB2();
    });
}

someAsync().then(function () {
    return asyncA();
}).then(function () {
    return asyncB();
}).then(function () {
    console.log("OK.");
}).catch(function(err) {
    console.error("Error:", err.message);
});

しかしついつい、以下のように新しいPromiseを作って返すコードを書いてしまうことがあるんです。 間違ってはいないけれど冗長ですし、これが積み重なると、まさに「プロミス地獄」が始まる気がする。

function asyncA() {
    return new Promise(function(resolve, reject) {
        asyncA1().then(function() {
            return asyncA2();
        }).then(function () {
            return asyncA3();
        }).then(function() {
            resolve();    
        }).catch(function(err) {
            reject(err);
        });
    });
}

function asyncB() {
    return new Promise(function(resolve, reject) {
        asyncB1().then(function() {
            return asyncB2();
        }).then(function() {
            resolve();    
        }).catch(function(err) {
            reject(err);
        });
    });
}

someAsync().then(function () {
    return asyncA();
}).then(function () {
    return asyncB();
}).catch(function(err) {
    console.error("Error:", err.message);
});

まとめ

アレっ?書き始める前はもっとあった気がするんですけどね。

思いついたら随時追記するつもりですー。