Disclaimer: None of this code was actually run!
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.
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.
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)->
# ...
)
)
)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.
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.
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!
userisn'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
@propertydoesn't exist yet - Check if
@propertyis loaded yet inlocalTimeand make something temporary up - Return
nullfromlocalTimeand 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.
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)
)
promiseNow everything is promised, and yes, any code looking for localTime needs to treat it as a promise too.
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.
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)
)
promiseNow 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.
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.
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.
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.
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:
