Using jQuery Deferred to wait for multiple Backbone models to save

Backbone's Model implementation is great for most things, but one thing I've had a hard time with is waiting for multiple models to save before proceeding. Backbone offers a success callback like this:

model.save
  success: ->
    alert("We did it!")

You could also use the sync callback like this:

model.on 'sync', ->
  alert("We did it!")
model.save()

But what about when you want to wait for multiple models to finish saving, all with their own asynchronous requests?

Don't Nest It. Chain It!

The jQuery Deferred object is a chainable utility object that can register multiple callbacks to relay the success or failure state of an asynchronous operation. Lucky for us, Backbone's model.save() method returns a jqXHR object, which implements the Deferred API. This means that instead of writing this:

model.save
  success: ->
    alert("We did it!")

We can write this:

model.save().done(-> alert("We did it!"))

That's a nice bit of syntactic sugar, but it still doesn't address our original problem: How can we wait for multiple models to save, and then fire the callback to alert the user?

Tell Me When You're All Done

jQuery.when allows us to combine multiple Deferred objects into one aggregate Deferred object, such that we can chain callbacks to be executed only when all the objects have resolved.

For sake of example, let's say we have a collection of 3 Backbone models we'd like to save:

collection = new MyCollection([{name: "Steve"}, {name: "Dave"}, {name: "Tom"}])

Remember that Backbone's model.save() returns a jqXHR object, which acts as a Deferred. So we can run:

xhrs = collection.map (model) -> model.save()

This will create an array xhrs containing the jqXHR objects for each individual save operation. To alert the user when all of them complete, we can use jQuery.when:

jQuery.when(xhrs...).done(-> alert("All of them are saved!"))

Note: The splat (...) syntax above is required to split the xhrs array into separate arguments. This had me stumped---without the splat, jQuery treats the array as a single Deferred object, which obviously doesn't execute the callbacks in the same manner as multiple jqXHR objects.

And Tell Me When One of You Failed

We can also use Deferred's fail() method to alert the user that one or more of the save operations failed:

jQuery.when(xhrs...).
  done(-> alert("We succeeded!")).
  fail(-> alert("We failed."))

Conclusion

The jQuery Deferred API is a powerful way to elegantly wait for the completion of asynchronous operations in your Backbone application. While it's tempting to resort to workarounds like using setTimeout to wait an arbitrary amount of time for operations to complete, using jQuery.when means you don't introduce race conditions into your application.

If you have any questions or if something isn't working as described above, please leave me a comment. I'll try my best to answer as soon as I can.