jQueryのajaxはdone,failでなくthenにすべき

# jQueryのajaxはdone,failでなくthenにすべき

とある諸事情から、jqueryを利用したサーバレンダリングなシステムの試験・バグ対応を担当することになった。
本来ならばコーディングを担当したベンダーで試験・バグ対応も行う予定であったのだが、
とある騒動で出勤停止措置などの外的要因が発端となり、急遽システムの試験・バグ対応のヘルプ要員に任命された。
一からコードを読んで即座にバグ対応するノウハウが求められる中、jqueryのajax処理について気づきがあったので今回記事にしてみることに。

# jQeuryのajaxの使い方(done/fail)
一般的には`jqXHR`の`done`や`fail`でレスポンス処理をコーディングするだろう。そのシステムもこういう書き方だった。

```js
$.ajax({
    url: "/sample.json"
}).done((data, textStatus, jqXHR) => {
    // 正常処理
}).fail((jqXHR, textStatus, errorThrown) => {
    // 異常処理
});
```

だが、試験を進めていくと、`done`処理内部に意図せず例外が発生するバグが含まれていることがわかった。

```js
$.ajax({
    url: "/sample.json"
}).done((data, textStatus, jqXHR) => {
    // 中略
    throw new Error("何らかの例外が発生する処理");
}).fail((jqXHR, textStatus, errorThrown) => {
    // 異常処理
});
```

この場合、throwされた例外はcatchされず、処理されないため、どこかで処理してやらなければならない。
一番容易に思いつく対策は、`done`の内部処理全部を`try-catch`することだろう。

```js
$.ajax({
    url: "/sample.json"
}).done((data, textStatus, jqXHR) => {
    try{
        // 中略
        throw new Error("何らかの例外が発生する処理");
    }
    catch(e){
        // 例外発生時の処理
    }
}).fail((jqXHR, textStatus, errorThrown) => {
    // 異常処理
});
```

だが、もしこのバグがすべてのajaxレスポンス処理で発生する可能性があるバグだとしたら・・・？
すべての`done`に`try-catch`を記載するには修正箇所が多すぎる場合、どうするのがよいだろうか。

# `$.ajax`のオーバーライド
一番最初に思いついたのは、jqueryが提供している`$.ajax`をオーバーライドして、独自の`$.ajax`にしてしまう、という方法。
`done`や`fail`は`$.Deferred()`を利用しているため、これを使えば「try-catchを共通化できるのでは？」と考えた。

```js
// $.ajaxを退避
var orgAjax = $.ajax;
// $.ajaxをカスタマイズする
function customAjax(ajaxArgs) {
    var settings = $.extend({}, $.ajaxSettings, ajaxArgs);
    var deferred_org = $.Deferred();
    var jqXHR_org = orgAjax(settings)
        .done(function (data, textStatus, jqXHR) {
            // 共通のdone処理
            try {
                // 個別のdone()を呼び出す（嘘）
                deferred_org.resolveWith(this, [data, textStatus, jqXHR]);
            }
            catch (e) {
                // 個別のdone内部で発生した例外の対処（嘘）
                console.error(e);
            }
        }).fail(function (jqXHR, textStatus, errorThrown) {
            // 個別のfail()を呼び出す（嘘）
            deferred_org.rejectWith(this, [jqXHR, status, errorThrown]);
        });
    return $.extend({}, jqXHR_org, deferred_org);
}
// $.ajaxを上書き
$.ajax = customAjax;
```

だが実際は期待通りにならなかった。これではdoneで`throw`された例外を`catch`できないのだ。

# Deferredを使った例外処理は`then().catch()`を使う
ここで登場するのが、`done()`や`fail()`ではなく`then()`である。

```js
// $.ajaxを退避
var orgAjax = $.ajax;
// $.ajaxをカスタマイズする
function customAjax(ajaxArgs) {
    var settings = $.extend({}, $.ajaxSettings, ajaxArgs);
    var deferred_org = $.Deferred();
    var jqXHR_org = orgAjax(settings)
        .then(
            function cmnDone(data, textStatus, jqXHR) {
                // 個別のdone()を呼び出す
                deferred_org.resolveWith(this, [data, textStatus, jqXHR])
            },
            function cmnFail(jqXHR, textStatus, errorThrown) {
                // 個別のfail()を呼び出す
                deferred_org.rejectWith(this, [jqXHR, textStatus, errorThrown]);
            }
        )
        .catch((e) => {
            // 個別のdoneで発生した例外をcatchできる
            console.trace(e);
        });
    return $.extend({}, jqXHR_org, deferred_org);
}
// $.ajaxを上書き
$.ajax = customAjax;
```

こうすれば、今までの`$.ajax`呼び出し箇所を変更することなく例外throwに対処できるようになった。

```js
$.ajax({
    url: "/sample.json",
})
    .done(function (data, textStatus, jqXHR) {
        // 例外をthrowしてもcustomAjaxで定義したcatchで処理される
        throw new Error("test");
    })
    .fail(function (jqXHR, textStatus, errorThrown) {
    })
```

# まとめ
今回の経験から得るべきことは

- `$.ajax`（というか`$.Deferred()`）のレスポンス処理は`then()`で書く
  - done内部に処理を書くときは、必ずdone内部に`try-catch`を書く
- `$.ajax`はオリジナルをそのまま使わない
  - 独自のajax関数でラップして使うべき