I Want It That Way (Promises Part II) – Zenefits Engineering


As we’ve discussed in Part I of this series, Javascript ain’t nothing but a heartache without a proper understanding of its fundamentals and a solution to keep Async `N Sync. Today, we review best practices for Promises in general and specifically in the context of Ember.js, all of which keeps our code two worlds apart from falling apart.

You are my fire
The one desire
Believe when I say
I want it that way

Will our series on promises ever finally resolve? Who knows– until then, let’s continue training our JS street smarts and exercising our mental gymnastics, studying technical concepts and working in references to our favorite 90’s music.

Avoid nesting promises

If possible, avoid nesting promise handling within promise handlers. Instead, try to return the promise from the success handler so that the successful .then chain may continue. This helps with readability when debugging promise-y code and allows us to chain towards a single point of failure.

When to use .catch

While not normally the case, using the .catch error handler at the bottom of your promise chain is entirely optional in our codebase. If undeclared and an error occurs in our promise chain, Zenefits’ global RSVP event handler logs error reports to Mode. When .catch is declared, any errors are must be explicitly logged via our UI eventLogger.
Ideally, .catch should be our single point of failure handling for the entire promise chain, unless we need very granular error handling for each failure possible in your chain.

When to use .finally

The .finally handler is run just once after either the successful .then handler or the failing .catch handler. The code we put into .finally is code that we want to run regardless of success or failure in our promise chain. Typically, this involves resetting an entire page or a loading button.

Avoid promises in computed properties

Sometimes, we want to fetch additional data asynchronously within our controllers. We use promises in the code triggered by asynchronous behavior such as a button press or observer on a property.
Avoid returning a promise out of a computed property. In addition to forcing us to treat that property like a promise everywhere in our controller, this can also cause unpredictable behavior when accessing that property. It’s likely that we could use Ember Observers to achieve the same result without computed property promises.

Use Ember.computed.alias with Ember.RSVP.hash as model

When returning an Ember.RSVP.hash from our model functions in the Ember Route, our model property in the Ember Controller will actually be a hash. It’s slightly inconvenient to have to prefix all model accesses with model.property so it may be convenient to introduce computed properties that alias off of the model hash at the top of our controllers.

Aliaising is preferred over setting properties onto our controllers via setupController in the Ember Route.

Avoid promise logic in the Route afterModel if possible

Typically, the afterModel function in the Ember Route is reserved for manipulating the data fetched from the model function. Very rarely will we need to fetch additional data in the afterModel that cannot be fetched via promises in the original model function. The reason we want to condense all promises into the model function is for page load speed. The afterModel function only executes after the promises from the model function are resolved.

Let’s imagine a scenario where promise A and B are resolved in the model and promise C is resolved in the afterModel. Promise C happens to depend on data returned from promise A (so it must wait for promise A to resolve first), and promise B is a very long promise (much longer than both A and C). The total time to load the page caused by pending promises would be roughly B + C, since promises A and B resolve in parallel in the model function and promise C waits in the afterModel. However, if we resolve all 3 promises in the model function, the page load time gets reduced to at most the time it takes to resolve promise B, since A + C is now resolving in parallel with B (assuming A + C is less time than B).


Route – Suboptimal Promises


Name arguments properly when handling the promise

It’s tempting to name the argument coming into your .then handler to be something like res or result. Don’t do that. Use a proper name!

Use Ember.RSVP.hash instead of this.all / MultiModelMixin if possible

A little bit of context is needed before diving into the reasoning for this one. And by a little bit, I mean we’ll have to understand the implementation, the Ember Route and Controller lifecycle, and everything mentioned before on this page.

In older versions of Ember, there was the notion of an ObjectController. The principle was that each page in an Ember app (represented by an ObjectController) would be anchored on one frontend model object. The downside to this was that we weren’t easily able to involve more than one frontend model on the page given Ember’s conventions. This inspired the creation of our MultiModelMixin, which provides the all function to be used in Ember Routes. The implementation of this mixin is below, along with an example use case:

Usage Example – this.all

In a gist, MultiModelMixin allowed us to pass multiple frontend models to our Ember Controller by using Ember.RSVP.hash and using controller.setProperties in the inherited setupController. Phew. This was a great utility when using ObjectControllers. However, in more recent versions of Ember, there has been a shift of preference towards vanilla Ember Controllers. With that new preference, there are a few disadvantages to our old MultiModelMixin:

Difficult to trace origin of controller properties – Because MultiModelMixin uses the inherited setupController (a magical method part of the Ember Route lifecycle) to set properties on the Ember Controller, no declarations of those properties exist in the Controller itself as everything is being set via the Route. While debugging a property on the Controller, it’s difficult to easily find where it’s these properties set.

We have an easily understood, more transparent, built-in alternative available for multiple modelsEmber.RSVP.hash (and Ember.computed.alias for convenience)
Very buggy when observers are involved – There is a huge bug when an Ember Controller implements an observer that listens on one of the properties passed into the second argument of this.all. Because MultiModelMixin uses setProperties to set properties on to the controller, our observer would actually fire before our controller is fully set up (assuming we call this._super at the top of our Route’s setupController). If we had additional setup code in our Route’s setupController that our observer depends on, we’d either have to write defensive checks on top of our observer function or call this.super at the bottom of our setupController function to avoid buggy behavior. Don’t try this at home!

SYNTACTIC SUGAR

We’ve written many helpers in our codebase to reduce the amount of code we need to write when working with promises.

thenpath

thenpath is a convenience function for resolving a deeply nested relational query off of a frontend model. For example, if you need to fetch user.company.onboardingSettings, that would typically be 3 potentially nested or chained promises that need to be resolved. thenpath generates this promise chain for us and returns a promise that resolves when the last link in the chain is resolved. The first argument passed into thenpath is the model object/promise that possesses the relationship we want to fetch. The second argument is a period delimited path of the relationship.

Usage Example – thenpath

Implementation – thenpath

wrapArrayPromise

wrapArrayPromise be a function that resolves an array of promises recursively (if the array contains more promise-y arrays), using Ember.RSVP.all.


Implementation – wrapArrayPromise

Think one of our best practices ain’t nothin’ but a mistake? We always wanna hear you say “I want it that way,” especially if you tell us why on Twitter (@ZenefitsEng) or Facebook.

Stay tuned for the next part in our continuing series on promises, where we’ll cover examples of promises in practice with a behind the scenes look how promises power components that are the fundamental building blocks of our product.

Thanks to Taras Mankovski (@EmberSherpa), Edward Zhang, Glynn Morrison, and James Hsi for reviewing drafts of this.



Source link