jQueryのDeferredとPromiseの違いを理解する

jQueryのDeferredはPromiseの「古い版」ではありません

よく「jQueryのDeferredはPromiseの前身」と説明されます。
半分は正しく、半分は誤解です。

jQuery DeferredはPromiseに似ていますが、同じものではありません。
そしてこの違いを理解しないままコードを書くと、非同期処理のバグに遭遇します。

結論から言うと、

  • Promise … 状態を監視する仕組み
  • Deferred … 状態を操作できてしまう仕組み

ここが最も重要な違いです。

Promiseとは何か(前提整理)

Promiseは「未来に値が決まる」ことを表すオブジェクトです。
通信やタイマーなどの非同期処理の結果を扱うために使います。

fetch("/api/user")
  .then(res => res.json())
  .then(user => console.log(user));

このとき開発者は「結果を受け取る側」だけを書きます。
Promiseの状態(成功・失敗)を変更することはできません。

つまりPromiseは「結果の通知専用オブジェクト」です。

Promiseの特徴:

  • 成功(resolve)は1回だけ
  • 失敗(reject)は1回だけ
  • 外部から状態変更不可
  • チェーン可能

ここが安全性を生みます。

jQuery Deferredとは何か

Deferredは、Promiseのように見える別の仕組みです。

var d = $.Deferred();

setTimeout(function(){
  d.resolve("OK");
},1000);

d.done(function(result){
  console.log(result);
});

一見Promiseと同じです。
しかし決定的な違いがあります。

resolveを外部から呼べます。

つまり「非同期の結果」を外から変更できます。

Promiseではこれは禁止されています。

なぜこの違いが重要なのか

Deferredは便利ですが危険です。
理由は「状態が壊れる」可能性があるからです。

例えば次のコードです。

function getUser(){
  var d = $.Deferred();

  setTimeout(function(){
    d.resolve("userA");
  },1000);

  return d;
}

var req = getUser();

req.done(function(u){
  console.log("1:", u);
});

req.resolve("userB");

この場合、通信結果とは無関係にuserBが出力されます。
呼び出し側が処理結果を上書きできてしまいます。

Promiseでは起こりません。
Promiseは外部から状態変更できない設計だからです。

これがDeferredの最大の問題です。

then / done / fail の違い

DeferredとPromiseはメソッドも微妙に違います。

メソッド Deferred Promise
then あり あり
done あり なし
fail あり なし
catch なし あり

特に重要なのは`done`です。

`done`はエラーを捕捉しません。

$.ajax("/api")
  .done(function(){
    throw new Error("失敗");
  });

この例では例外がチェーンに流れません。
デバッグが難しくなります。

Promiseなら`catch`に流れます。

fetch("/api")
  .then(()=>{ throw new Error(); })
  .catch(()=> console.log("捕捉"));

この挙動差は、実務でトラブルの原因になります。

jQuery 1.x〜2.x時代の背景

なぜこのような仕組みになったのでしょうか。

理由は歴史です。

Deferredが作られた当時は、まだPromise仕様(Promise/A+)が確立していませんでした。
非同期処理の標準設計が存在しなかったのです。

そのためjQueryは独自設計で非同期管理を提供しました。

つまりDeferredは「間違った実装」ではありません。
標準が存在しない時代の解決策です。

Promise互換になったのはjQuery3以降

jQuery 3以降、DeferredはPromiseに近づきました。
しかし完全互換ではありません。

特に注意が必要なのは以下です。

  • 例外の伝播
  • 同期resolve時の挙動
  • thenの戻り値

古いjQueryコードをES6 Promiseと混在させると、不思議なバグが発生します。

注意点:DeferredをPromiseだと思って使うと壊れる

典型的な問題です。

return $.ajax("/api").then(res => res.data);

これをPromiseだと思ってasync/awaitで使うと、
例外処理が期待通り動かないケースがあります。

つまり、

Deferred ≠ Promise
Promise風のオブジェクト

です。

ではどう扱うべきか

既存コードでDeferredがある場合は、Promiseに変換するのが安全です。

function toPromise(jqXHR){
  return new Promise(function(resolve,reject){
    jqXHR.done(resolve);
    jqXHR.fail(reject);
  });
}

これによりES6の非同期管理に統一できます。

まとめ

jQuery DeferredとPromiseは似ていますが設計思想が違います。

Deferredは「操作可能な非同期状態」、
Promiseは「通知専用の非同期状態」です。

jQueryの非同期処理が難しく感じる理由の多くは、この違いにあります。
そしてasync/await時代に移行する際、最もつまずきやすいポイントもここです。

Deferredは過去の遺産ではなく、
JavaScriptの非同期設計が進化してきた歴史そのものと言えます。