jQuery の Deferred といわゆる Promise/A+ について、ごっちゃになってたのでメモ。
then/catch で発生した例外は reject された Promise になる
jQuery 3 から? Promise/A+ 互換になったため then/catch
で発生した例外は reject された Promise として次のチェインに渡る。
$.Deferred().resolve(1).promise() .then((v) => { console.log('then', v); // then 1 throw 2; }) .catch((v) => { console.log('catch', v); // catch 2 })
なので catch
で処理しない例外は静かに無視される。
$.Deferred().resolve(1).promise() .then((v) => { console.log('then', v); // then 1 throw 2; }) .then((v) => { console.log('then', v); // never })
Node.js の Promise ならそういう状況では以下のような警告が表示される。
(node:87126) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): 2 (node:87126) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
そうではない処理系のために Promise のライブラリによっては Promise#done
というメソッドが設けられていることがあるらしい。
要するにチェインの最後で .done()
しておけばキャッチされない例外は Promise の外まで飛んで行くようになる。
一方で jQuery の done/fail/always
で例外が発生した場合はそのまま Promise の外まで飛んで行く。done
の動きは↑の説明とぜんぜん違うので注意(done
は Promise/A+ の仕様ではないので jQuery が特別おかしいわけではない)。
$.Deferred().resolve(1).promise() .done((v) => { console.log('done', v); // done 1 throw 2; // uncaught exception: 2 }) .fail((v) => { console.log('fail', v); // never }) .always((v) => { console.log('always', v); // never })
jQuery で then
しつつ catch
されない例外を Promise の外に持っていきたければチェインの最後で次のようにすると良いだろう。
$.Deferred().resolve(1).promise() .then((v) => { console.log('then', v); // then 1 throw 2; }) .then((v) => { console.log('then', v); // never }) .fail((err) => { throw err; // uncaught exception: 2 })
jQuery の done/fail/always の戻り値は Promise の状態を変えない
then
や catch
はコールバック関数が返した値で解決された新たな Promise を返す(コールバックが Promise を返したならその Promise の状態と値を持つ新たな Promise が返る)。
$.Deferred().reject(1).promise() .catch((v) => { console.log('catch', v); // catch 1 return 2; }) .then((v) => { console.log('then', v); // then 2 return 3; }) .then((v) => { console.log('then', v); // then 3 return $.Deferred().reject(4).promise(); }) .catch((v) => { console.log('catch', v); // catch 4 })
一方で done/fail/always
の戻り値は元の Promise のまま。
var promise = $.Deferred().resolve(1).promise(); console.log(promise === promise.done(() => {})); // true console.log(promise === promise.then(() => {})); // false
そのためコールバックが何を返してもチェインされる Promise の状態や値は変わらない。
$.Deferred().reject(1).promise() .always((v) => { console.log('always', v); // always 1 return 0; }) .fail((v) => { console.log('fail', v); // fail 1 return 2; }) .done((v) => { console.log('done', v); // never return 3; }) .done((v) => { console.log('done', v); // never return $.Deferred().reject(4).promise(); }) .fail((v) => { console.log('fail', v); // fail 1 })
jQuery の Promise は複数の値を持てる
本来 Promise/A+ では1つの値しか持てない。
Promise.resolve(1, 2, 3) .then((a, b, c) => { console.log('then', a, b, c); // then 1 undefined undefined })
jQuery の Promise は複数の値を持てる。
$.Deferred().resolve(1, 2, 3).promise() .then((a, b, c)=>{ console.log('then', a, b, c); // then 1 2 3 })
then
や catch
をチェインするときに次の Promise に複数の値を持たせたいときは複数の値を持つ jQuery の Promise を返せば良い。
$.Deferred().resolve(1, 2).promise() .then((a, b)=>{ console.log(a, b); // 1 2 }) .then((a, b)=>{ console.log(a, b); // undefined undefined return $.Deferred().resolve(3, 4) }) .then((a, b)=>{ console.log(a, b); // [3, 4] })
さいごに
昨今はバベれば async/await
が非常に書きやすいので、jQuery 独特の仕様に依存しないよう $.ajax
をラップした関数で Promise が1つの値だけを持つようにして、async/await
前提で書くのが良いと思う。
function ajaxGet(url, params) { return $.ajax(/* ... */) .then( (data, textStatus, jqXHR) => { // data を見て(必要なら jqXHR とかも)リクエストの成否を判断する if (err) { // リクエストが失敗しているなら失敗の理由を示すなにかを例外として投げる throw err; // こっちのほうが良いかも return Promise.reject(err); } // リクエストが成功したならその結果を返す return data; }, (jqXHR, textStatus, errorThrown) => { // リクエストの失敗を示すなにかを例外として投げる throw err; } ) } async function handler() { try { const data = await ajaxGet('/path/to/api', {}); // .done() の処理 } catch (err) { // .fail() の処理 // 握りつぶさないように再送 throw err; } finally { // .always() の処理 } }
バベれないときは then
や catch
で発生した例外が闇の彼方に葬られないように気をつける必要がある。
↑の方で書いたように then
や catch
の後で fail
で例外を throw
するか、catch
で setTimeout(function () { throw err }, 0)
とかだろうか。