Skip to content

Instantly share code, notes, and snippets.

@justinjmoses justinjmoses/Deferred.js
Last active May 11, 2018

Embed
What would you like to do?
Migrating to jQuery 3 Promises A+

Migrating to jQuery 3 Promises A+

There are a number of pernicious issues when migrating to jQuery 3 with respect to how the Promises A+ spec changes. Compare jQuery 2, 3 and native Promises in this Codepen

Major notes:

  • All resolutions happen on the next callstack (no more sync). This is a major issue in tests that regularly assume $.Deferred.resolve() will be resolved synchronously. The solution in mocha is to either return the promise to the beforeEach handler to chain. If multiple promises are resolved in the underlying code .then() chains need to be added to the return chain for each promise handler. Another solution is to use a https://github.com/YuzuJS/setImmediate wrapper for the returned Promise.

  • Errors in Promises are now handled by throw. You can return a rejected deferred ($.Deferred.reject(...)) but it's preferable to use the native throw.

  • If you add an error handler (either via then(..., err => ...) or catch(err => ...)) to a promise chain and you do NOT throw again, the promise chain is considered resolved. As such, throw another error if you want the chain to stay rejected.

    // the below propagates the error in jQuery 2 and resolves the promise chain in jQuery 3.
    $.Deferred().reject(123)
      .then(() => {}, err => 'I am the new error message')
      .then(newErr => console.log('I am called in jQuery 3'), newErr => console.error('I am called in jQuery 2'))
    
    // for the desired behavior, throw the error in jQuery 3
    $.Deferred().reject(123)
      .then(() => {}, err => { throw 'I am the new error message' })
      .then(console.log, console.error)
  • Cannot resolve or reject with multiple arguments with Promises (Promise.resolve/reject vs Deferred.resolve/reject).

    // jQuery Deferred allows for non-compliant multi-argument resolution/rejection
    $.Deferred().reject(1,2,3).catch((a,b,c) => {
      console.error("jQuery 3 Deferred rejects with ", a, b, c); // 1 2 3
    })
    
    // Native promises ignore extra arguments
    Promise.reject(1,2,3).catch((a,b,c) => {
      console.error("Native Promise can only reject with on value: ", a, b, c); // 1
    })

Minor Notes to Team

  • use Promise.all not $.when (requires arrays)

    // so
    $.when(whenA, whenB).then((a, b) => {...})
    
    // becomes
    Promise.all([whenA, whenB]).then(([a, b]) => {...})
  • no more done / fail when you use native Promises (always can be handled with finally).

  • no more promise.state when you use native Promises.

Testing With Mocha Notes

  • Use Deferred utility class (below) if you want native Promises with .resolve() .reject() functionality in tests. Note you'll lose the ability to resolve/reject with multiple values (which we should avoid anyhow), plus you cannot use done fail or always.

  • Mocha will complain if you return a rejected promise in a beforeEach or it block. However, if you catch the error in the promise chain, you may prevent your code from receiving the error via a stub. Therefore, if you want to use a rejected promise as the return value of a Mocha beforeEach or it and in the return value of a stub, catch the error in the return Mocha statement after you’ve already given the rejected promise to your stub

    beforeEach(() => {
      const promise = Promise.reject();
                      
      sinon.stub(..., promise); // don't do `sinon.stub(..., Promise.reject().catch(() => {})) otherwise that will resolve
                        
      // ensure mocha waits for next tick and prevent node from complaining about rejected promises
      return promise.catch(() => {}); 
    })
  • One way to test in mocha is to use a async test callback, remembering that if called with any argument, that becomes the error

    it('resolves correctly', done => {
      promise.then(() => done(), done)
    })
    
    it('rejects correctly', done => {
      promise.then(() => done('Should not have resolved!'), () => done())
    })
module.exports = class Deferred {
constructor() {
this._promise = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
}
resolve(value) {
this._resolve(value);
return this;
}
reject(value) {
this._reject(value);
return this;
}
promise() {
return this._promise;
}
then(...args) {
return this._promise.then(...args);
}
catch(...args) {
return this._promise.catch(...args);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.