This I Promise You (Promises Part I) – Zenefits Engineering


From the perspective of its fundamental language features (many argue bugs!), Javascript’s current day popularity comes as a total surprise. Closures, the keyword this, and prototypal inheritance may be confusing concepts to newcomers and odd opinions to experienced programmers new to the language.

Given the async nature of the applications written with Javascript, a question arises:
How do we keep our async code ‘N Sync across our codebase without bringing tears to our eyes?

Every word I say is true
This I promise you
This I promise you

Amidst an asynchronous cacophony of callbacks and events, the promise pattern is a solution that restores harmony to the JS galaxy.

Part I of this series on Promises covers Promise basics, with a specific focus on practical use cases with EmberJS.

What Are JS Promises?

Javascript promises provide a structured mechanism for handling asynchronous behavior, typically with a binary outcome (resolved or rejected).
You can read about why they exist here: http://www.html5rocks.com/en/tutorials/es6/promises/.
Promises are built into ES5 Javascript (MDN). However, many JS libraries implement their own variation with additional functionality.

Use Cases in Ember

In Ember, we use the RSVP promise library.

In the basic usage example above, myExamplePromise could be any asynchronous JS process such as fetching a model relationship through Ember Models/TastyPie or an Ember.ajax call.

Ember Route Example

You’ll most often be dealing with promise behavior when fetching data asynchronously in your Route files through Ember Models/TastyPie or any custom API calls.

In the Ember Route example above, we’ve fetched a few relational properties off of the company object. With each relational property on any model object comes the possibility of Ember Models making a REST API call to fetch that data if it hasn’t already done so. Hence, we should always aim to resolve any relationships we need amongst our models by prefetching them in our Route file with few exceptions. If we successfully prefetch them in our Route file, we can use them without worrying about promisey behavior when in the Controller.

With breakdowns, we’ve actually fetched a nested relation (company has proposals, and proposals have breakdowns) using promise chaining.

You’ll also notice that the model returns an Ember.RSVP.hash which is a promise by itself. Ember will wait for the promise returned out of the model function in your Route to resolve before continuing onto Controller logic. More detailed Ember.RSVP.hash usage is provided below.

Ember Controller Example: Saving Models

Often, asynchronous behavior must be handled by the Controller as well. For example, a user could click a button on the page that requires you to save your models before progressing. The act of saving a model is an asynchronous process due to the REST call being made by Ember Models to TastyPie.

Ember.RSVP.hash
Often, we need to await the resolution of multiple promises at once, yet still be able to access their results conveniently afterwards. Ember.RSVP.hash allows us that functionality.

Ember.RSVP.hash is most typically used in the model function of an Ember Route file. An Ember Route’s lifecycle would wait for any promise returned out of the model function to resolve before continuing to afterModel and setupController. Often, Ember.RSVP.hash is used to return multiple “models” out of the model function to be accessed in the Controller. The resolved result of Ember.RSVP.hash is a hash of resolved content with keys as originally specified when it was declared.

If one of the values passed in the declaration of Ember.RSVP.hash is not a promise, it will simply be carried through to the returned hash, and nothing bad will happen.

Ember.RSVP.all

Similar to Ember.RSVP.hash, Ember.RSVP.all can also resolve multiple pending promises. Instead of passing a hash, we would instead pass an array of promises.

For accessing non-sequential data, Ember.RSVP.hash would be preferred over Ember.RSVP.all. Typically, we would use Ember.RSVP.all when you need to continue to iterate through the results or have a mix of pending promises you don’t care to access after resolving.

If one of the values passed in the declaration of Ember.RSVP.all is not a promise, it will simply be carried through to the returned list, and nothing bad will happen.

Ember.RSVP.resolve / Ember.RSVP.reject

Sometimes, you’ll want to provide promise-like behavior without actually performing anything asynchronous. This may happen when you want to conditionally return a promise in a promise chain for example. In order to keep your code clean and maintain the structure of the promise chain, you can use Ember.RSVP.resolve and Ember.RSVP.reject to return successful and failed promises, respectively.

A popular use case for this pattern in our codebase is validations. Typically, you may have a validator or validation function that returns a boolean. Before we enter your promise chain, we’ll likely have an if statement checking the result of the validation. Instead of doing the conditional, we can kick off our promise chain from the validation call. This allows us to have a single point of error handling (the .catch handler for your promise chain) for validation all the way through saving models. An example of the improvement in cleanliness is below:

Validations – Unchained Promises

Validations – Chained Promises

Ember.ajax / .ajaxGET / .ajaxPOST / .getJSON
Ember gives us access to the JQuery library for DOM manipulations and making network requests. We often make AJAX requests by using Ember’s JQuery in our code. Previously, we’ve simply called Ember.$.ajax and handled the promises accordingly. However, when writing our acceptance tests, Ember needs to know when asynchronous behavior is happening in its system. Because JQuery promises are not Ember.RSVP promises, Ember isn’t able to detect the asynchronous behavior that $.ajax introduces.

An ellusive engineer’s stream of consciousness on the matter:

JQuery promises (like the ones generated by Ember.$.ajax and Ember.$.getJSON) are not compliant with the promise A+ spec. They have different error handling and do not resolve thenable return values before passing them to then handlers, and synchronously call then handlers when the promise is already resolved, but asynchronously calls then handlers when the promise has not resolved, which basically unleashes Zalgo. In this code and in many other parts of our codebase we use Ember.RSVP.resolve to assimilate the jQuery promise into an A+ compliant promise. This is now simpler easier thanks to Ember.ajax and Ember.getJSON stubs.

To resolve this, we’ve created wrapper functions Ember.ajax, .ajaxGet, .ajaxPost, and .getJSON that behave as expected from the original JQuery methods except using Ember.RSVP promises instead.

Tune in to the next part of this series, where we’ll continue by discussing our suggested practices, common gotchas, and how we break JS promises.

– Dennis Qian
Thanks to Peter Pong, Jonathan Collins, Ed Zhang, and James Hsi for reviewing drafts of this.



Source link