Skip to content

Instantly share code, notes, and snippets.

@kristjan
Created October 15, 2014 03:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kristjan/ababfdee2edf0b39314c to your computer and use it in GitHub Desktop.
Save kristjan/ababfdee2edf0b39314c to your computer and use it in GitHub Desktop.
Promises in Angular

Disclaimer: None of this code was actually run!

What's a Promise

A way to handle something that happens asynchronously. Instead of getting your result back, you get a promise back that you can handle in a success way and a failure way. At some point in the future, the promise resolves to one of those cases, passing your success/failure functions the applicable data.

Why use promises?

Unless you want to block your application thread, you need to allow asynchronous tasks to run in parallel with other work like responding to user events. Various languages have various ways to do this, but in JavaScript, your options are basically Callbacks and Promises.

Callbacks

In the callback model, you pass your async function another function to call when it's done. The function you call does not have any return value, and what would have been returned is instead passed to your callback, typically with any error as the first argument (undefined if all is well).

order = {meat: 'ham', bread: 'dutch crunch'}
getSandwich order, (err, sandwich) ->
  if err
    alert("Oh no! #{err}")
  else
    eat(sandwich)

One of the things people dislike about the callback pattern is that one can quickly get nested deeply into callback hell.

foo((err, foo_result) ->
  if (err)
    alert(err)
  else
    bar(foo_result, (err, bar_result) ->
      if (err)
        alert(err)
      else
        baz((bar_result), (err, baz_result)->
          # ...
        )
    )
)

Promises

Using promises, you get a promise object back from the function call, which you can then add success and failure functions to that will be run when the promise resolves.

getSandwich(order)
  .then(eat)
  .catch(alert)

catch handles any error that happens during chained promises (you can also pass failure functions to each then independently). That and the syntax help you stay unnested and more readable.

foo
  .then(bar)
  .then(baz)
  .then(baf)
  .catch(alert)

Every then is returning a new promise that is resolved by the function you passed.

Async is Contagious

You can not write synchronous code that depends on asynchronous code. If you disagree, first think through this real-world case:

  • You: Kristján, would you go get me a sandwich? Call me when you're back.
  • Me: Sure, I'll be back later
  • You: Sweet, I'm going to start eating this sandwich

NO! I haven't returned with your sandwich yet!

So conceptually, I hope you're on board. Now look at the code examples.

Consider if one of the functions in the callback case returned a value instead of calling the callback with it. It's too late! The code that called the function has moved on and may have gone out of scope altogether. There's nowhere to return a value to, and the callback you were supposed to call is the only thing capable of handling the result.

Now in the promise case, imagine if one of the functions returned a resolved value instead of a new promise. You've just broken the whole chain (can't call then anymore), and the function you passed to the next then will never be called because there's no promise around anymore to resolve it.

So once you have an asynchronous piece of code, everything that depends on it must be asynchronous. There is no sane or even workable way to have sychronous code depend on asynchronous behavior.

A BedROC use case

BedROC needs to load things from the API. Sometimes those things depend on other associated things. There are places we're trying to depend sychronously on some of these API calls, which I hope is clearly dangerous:

printSomeone = ->
  user = null
  User.get(1).then((response) ->
    user = response
  )
  $log.info(user.name)

No! user isn't loaded yet!

The cases we're running into are those where some object is supposed to contain, or has some dependency on, another object. There's some instinct to lazy-load the dependent object that ends up trying to mix synchronous code with asynchronous code.

Tour::localTime = ->
  return TimeUtil.localTime(tourAt, property.timeZone)

Tour::property = ->
  return @property if @property?
  Property.get(propertyId).then((property) ->
    @property = property
  )
  return {timeZone: 'Why not Pacific for now?'}

Lovely, so now the first few times we look for the local tour time, we'll pretend the property is Pacific, because our other option was to

  • Crash because @property doesn't exist yet
  • Check if @property is loaded yet in localTime and make something temporary up
  • Return null from localTime and make whoever called it deal with that

If you use tour in some function that calls localTime thrice, the value of localTime can change partway through. Good luck reasoning about that.

Propagate the Promise

If you need a value that depends on something fulfilled via promise, the function that generates the value must also return a promise. Here's the sane version of localTime.

Tour::localTime = ->
  promise = $q.defer()

  loadProperty.then(->
    promise.resolve(TimeUtil.localTime(tourAt, property.timeZone))
  )
  
  promise

Tour::loadProperty = ->
  promise = $q.defer()

  if @property
    promise.resolve(@property)
  else
    Property.get(propertyId).then((property) ->
      @property = property
      promise.resolve(@property)
    )
  
  promise

Now everything is promised, and yes, any code looking for localTime needs to treat it as a promise too.

Angular Handles Promises

Up at the top-level, probably some view trying to render something, Angular will deal with promises for you. So your view code doesn't need to be full of then, nor does the controller need to use then to set any scope variables. The view can simply

Local time: {{ tour.localTime }}

If localTime has no async dependencies, Angular will print it. If it needs async work and returns a promise, Angular will update when the promise resolves.

Isolate Resolution to Initialization

You can use promises to ensure everything is loaded, then all your code becomes synchronous. The above code might load the Tour like so:

# TourController
$scope.tour = null

$scope.init = ->
  Tour.get(1).then((tour) ->
    $scope.tour = tour
  )

All well and good, but all our code that needs localTime is going to have to handle the promise, and the asynchronous dependency will propagate further.

The other way is to load what the Tour needs immediately, and not resolve the initial promise until we're ready:

$scope.tour = null

$scope.init = ->
  Tour.get(1)
    .then(loadProperty)
    .then((tour) ->
      $scope.tour = tour
    )

loadProperty = (tour) ->
  promise = $q.defer()

  Property.get(tour.propertyId).then((property) ->
    tour.property = property
    promise.resolve(tour)
  )
  
  promise

Now when $scope.tour is set and init is finished, everything is available. Since tour.property is guaranteed to be loaded, localTime is now a synchronous operation, and any code depending on it can just use it.

Dependencies with railsResourceFactory

Making the controller know and load all of the model's dependencies is obviously not fun, especially if the model is reused in lots of controllers that now all have to know. railsResourceFactory providers interceptors on the HTTP response that allow us to encapsulate this in the model, so instead of TourController needing to know that the tour needs a Property, we declare an interceptor on the Tour load that also loads its Property before resolving the initial promise.

Now we're doing a million requests all the time!

Who cares? It's async; it's happening while other assets are loading, while the Specialist's brain is loading the page we're showing them, etc. It takes a couple seconds, and that's perfectly ok.

No, seriously, this is way too many network requests

Eventually, we need to optimize, and that will require us drawing a line between objects that are loaded immediately as dependencies and other objects that aren't always needed and can be promised later when they are. But, we must choose

  • This object is loaded at initialization and available synchronously
  • This object is not needed often enough and is only available through promises

We cannot make one thing available both ways. If we don't want to write promise code to get at a value, it must be loaded before any of our page code starts executing. If we don't want to eager-load it because we don't want it often enough, it can only be accessible via promise.

References

I surely didn't type all the details right, or maybe you just still don't believe me. Here are some smarter, more eloquent people who might help:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment