backbone icon indicating copy to clipboard operation
backbone copied to clipboard

Return a rejected promise instead of false on invalid model save

Open akre54 opened this issue 11 years ago • 21 comments

From the discussion in #2345, returning false from a failed save prevents a clean chaining API (model.save().done(...).fail(...)). Using deferreds, we can reject a promise instead to avoid the discrepancy.

This pull doesn't introduce any new options vars (i.e. isValid), instead suggesting a separate event listener for the model's invalid event or querying model.isValid() directly within the failed handler. The invalid event will always be fired before the deferred is rejected and returned. The fail handler is fired for both failed validation as well as any ajax errors which makes it difficult to mutate the options object or keep the two callbacks' arguments consistent.

akre54 avatar Apr 16 '13 21:04 akre54

If we're going to allow swapping the promise lib, it would be great if that applied to all ajax instances...

sync: function() {
  return Backbone.Deferred().when(Backbone.sync.apply(this, arguments));
}

Since jQuery's promises don't follow the promises spec... I would really, really love to see this change.

tgriesser avatar Apr 18 '13 20:04 tgriesser

@tgriesser I would want to stay away from relying on when and stick to the generic reject, resolve, promise API... when isn't in every promise implementation and in any case in jQuery it lives directly on the $, not on $.Deferred()

wookiehangover avatar Apr 19 '13 21:04 wookiehangover

@wookiehangover - ah right, it does live on $... I guess I was thinking that it'd be nice if we were going to use promises (outside of the ones included in jqXHR), and you want to swap it for an implementation that works properly (not jQuery's), it'd be nice to have that switch be automatically applied across the board... but I guess it only takes a simple shim of Backbone.ajax to do that...

tgriesser avatar Apr 19 '13 21:04 tgriesser

Ran into this again today when I forgot you can't assume a promise on the save. I think it'd be a nice enhancement.

tgriesser avatar Jun 04 '13 15:06 tgriesser

@jashkenas can we get a go/no go on merging this?

wookiehangover avatar Jun 05 '13 17:06 wookiehangover

I have to say, this one makes me a bit nervous. Despite returning promises, Backbone doesn't currently use them. While it might be slightly nicer to get a promise instead of false (and this is certainly debatable), it's a giant can of worms to open for a fairly small issue.

braddunbar avatar Jun 05 '13 17:06 braddunbar

@braddunbar which is why I closed the original issue in the first place.... @jashkenas opened it back up in the interest of having a cleaner api.

But there are several things that fall out of this, including breaking out-of-the-box compatibility with Zepto once and for all (which has already happened IMO, but I digress)

wookiehangover avatar Jun 05 '13 17:06 wookiehangover

Yep, Zepto and any other library with a similar API. Good point.

braddunbar avatar Jun 05 '13 17:06 braddunbar

Yes -- I'm fairly far towards leaning towards a "no go" on this ... but I haven't sat down and thought about it as much as I'd like to just yet. Maybe tag as a change wontfix, but leave it open a little longer?

jashkenas avatar Jun 07 '13 08:06 jashkenas

As @ErichBSchulz pointed out in #2597, jqXHR's success, error, and complete ~~callbacks~~ methods were deprecated in jQuery 1.8 in favor of the promises API, and may be removed in a future version. I agree Backbone should generally keep its API surface small, but I think this solution is much more elegant when dealing with these async actions. Checking for a false return value separate from a server / network failure just feels like a kludge.

To @braddunbar's point, adding extra stuff is always a slippery slope, but the advantage is this opens Backbone up to do a lot better with success callbacks along the Backbone.sync path (no more var success = options.success cascades).

Zepto support is easily shimmed with Simply Deferred.

edit: clarified callbacks wording. The methods on jqxhr are deprecated, not the passed-in options.

akre54 avatar Jun 09 '13 22:06 akre54

Closing this particular PR. I'd still be happy to look at and discuss a more holistic approach to pervasive promises, in concert with a version bump ... but it looks like this patch is incomplete.

jashkenas avatar Oct 10 '13 19:10 jashkenas

It seems like the best solution in the interim is to wrap all save/destroy calls with Q/when/etc. then, right? That is, if you cared about promise correctness you probably wouldn't be using jQuery's promises in the first place...

akre54 avatar Oct 11 '13 13:10 akre54

I still think that this PR is independent of #3582 The only thing that we should agree that save and destroy should always return thenables.

Thus, if we combine this PR and #3582 it will look like:


Backbone.newDestroyed = function(model) {
    return Backbone.Deferred().resolve(model);
}

Backbone.notValidated = function(model) {
    return Backbone.Deferred().reject(model.validationError);
}

There also a question: should the obtained xhr be also wrapped into Backbone.Defered().resolve(xhr)? To unify all thenables?

Artazor avatar Apr 29 '15 07:04 Artazor

I see this as solving the root problem brought up by #3582. Additionally, it only adds one method onto the Backbone namespace, and it's a well-defined class (deferreds are easy to create from Promise libraries).

There also a question: should the obtained xhr be also wrapped into Backbone.Defered().resolve(xhr)? To unify all thenables?

So that all thenable return values are the same class, yes we should. Imagine overriding Backbone.Deferred to use native promises:

Backbone.Deferred = function() {
    var deferred = {};
    deferred.promise = new Promise(function(resolve, reject) {
        deferred.resolve = resolve;
        deferred.reject = reject;
    });
    return deferred;
};

I'd hate for some return values to be jQuery promises and some to be native promises.


As a side note, I don't think we should return deferred.resolve(). #resolve and #reject return a non-standard value (the deferred, vs undefined), and that the deferred object has #then is non-standard.

jridgewell avatar Apr 29 '15 14:04 jridgewell

So why Deferred? Why not Backbone.Promise? And use pretty sandard Backbone.Promise.resolve(value) and Backbone.Promise.reject(reason) ?

There is widespread belief that you should avoid using deferreds at all.

Artazor avatar Apr 29 '15 15:04 Artazor

So why Deferred? Why not Backbone.Promise?

Because jQuery provides a Deferred class, so we don't have to write a ton of extra code.

There is widespread belief that you should avoid using deferreds at all.

Hm?

jridgewell avatar Apr 29 '15 15:04 jridgewell

Opening for more discussion ... but we'd still need a real patch+proposal.

jashkenas avatar May 13 '15 20:05 jashkenas

I would strongly advocate for the Backbone.Promise that should provide only two static methods resolve and reject

declare module Backbone {
    ...
    export interface Thenable<T> {
        then<U>(
             fulfilled: (result: T) => U | Thenable<U>,
             rejected: (reason: any) => U | Thenable<U>
        ): Thenable<U>
    }
    ...
    export class Model<TSync> {
          save(...): Thenable<TSync>
          fetch(...): Thenable<TSync>
          destroy(...): Thenable<TSync>
          sync(...): Thenable<TSync>
    }

    export class Collection<TModel extends Model, TSync> {
          fetch(...): Thenable<TSync>
          sync(...): Thenable<TSync>
    }

    export function sync<TModel, TOpts, TSync>(
          method: string, 
          model: TModel, 
          options: TOpts
     ): Thenable<TSync>

    export class Promise {
         // lets rely only on those two static methods:
         static resolve<T>(value: T | Thenable<T>): Thenable<T>;
         static reject<T>(reason: any): Thenable<T>;
    }
}

Inside you can use promises created by jQuery.Deferred by default or the same jqXHR but Backbone itself should rely only on two standard static methods that are placed under Backbone.Promise: resolve and reject.

My motivation:

  • Those who never want to override methods will get the same jqXHR and JQueryPromises by the default implementation. Note that we do not need to implement promises in Backbone, we will reuse jQuery Deferred.promise() or similar things
  • Those who wnat to override this, probably want to use Promises/A+ implementation and would be able to do it with single: Backbone.Promise = MyFavoritePromiseClass

Artazor avatar May 13 '15 20:05 Artazor

I agree fully with @Artazor here (and thanks for the detailed interface). Do you propose we wrap sync (or ajax) in Backbone.Promise or should the implementing user's ajax method be required to return a thenable?

It'd be awesome to see a pull with this implementation if you have time.

akre54 avatar May 15 '15 17:05 akre54