In Part 1, we looked at the awkward situation created when we introduce callbacks to handle even a single asynchronous operation into an otherwise simple set of function calls.
- We can no longer use a simple call-and-return programming model
- We can no longer handle errors using try/catch/finally
- We must add callback and errback parameters to every function signature that might eventually lead to an asynchronous operation
A Promise (aka Future, Delayed value, Deferred value) represents a value that is not yet available because the computation that will produce the value has not yet completed. A Promise is a placeholder into which the successful result or reason for failure will eventually materialize.
Promises also provide a simple API (see note below) for being notified when the result has materialized, or when a failure has occured.
(NOTE: Although there are several proposed Promise API standards, Promises/A has been implemented in several major frameworks, and appears to be becoming the defacto standard. In any case, the basic concepts are the same: 1) Promises act as a placeholder for a result or error, 2) they provide a way to be notified when the actual result has materialized, or when a failure has occurred.)
The Canonical XHR Example
In the case of an XHR Get, the value we care about is the content of the url we’re fetching. We know that XHR is an asynchonous operation, and that the value won’t be available immediately. That fits the definition of a Promise perfectly.
Imagine that we have an XHR library that immediately returns a Promise, as a placeholder for the content, instead of requiring us to pass in a callback. We could rewrite our asynchronous
thisMightFail function from Part 1 to look like this:
Now, we can return the Promise placeholder as if it were the real result, and our asynchronous
thisMightFail function looks very much like a plain old synchronous, call-and-return operation.
Taking Back the Stack
In a non-callback world, results and errors flow back up the call stack. This is expected and familiar. In a callback-based world, as we’ve seen, results and errors no longer follow that familiar model, and instead, callbacks must flow down, deeper into the stack.
By using Promises, we can restore the familiar call-and-return programming model, and remove the callbacks.
Now let’s introduce the asynchronous
thisMightFail from above that uses our Promise-based XHR lib.
getTheResult() is identical in the synchronous and asynchronous cases! And in both, the successful result or the failure will propagate up the stack to the caller.
Notice also that there are no callbacks or errbacks (or alwaysbacks!) being passed down the callstack, and they haven’t polluted any of our function signatures. By using Promises, our functions now look and act like the familiar, synchronous, call-and-return model.
We’ve used Promises to refactor our simplified
getTheResult function, and fix two of the problems we identified in Part 1. We’ve:
- restored call-and-return
- removed callback/errback/alwaysback parameter propagation
But, what does this mean for callers of
getTheResult? Remember that we’re returning a Promise, and eventually, either the successful result (the result of the XHR) or an error will materialize into the Promise placeholder, at which point the caller will want to take some action.
What about the Caller?
As mentioned above, Promises provide an API for being notified when either the result or failure becomes available. For example, in the proposed Promises/A spec, a Promise has a
.then() method, and many promise libraries provide a
when() function that achieves the same goal.
First, let’s look at what the calling code might look like when using the callback-based approach:
Now, let’s look at how the caller can use the Promise that
getTheResult returns using the Promises/A
Or, more compactly:
(Image from The Meta Picture)
Wasn’t the whole point of this Promises stuff to avoid using callbacks? And here we are using them?!?
Stay with Me
However, there are several important advantages in using Promises over the deep callback passing model from Part 1.
First, our function signatures are sane. We have removed the need to add callback and errback parameters to every function signature from the caller down to the XHR lib, and only the caller who is ultimately interested in the result needs to mess with callbacks.
Second, the Promise API standardizes callback passing. Libraries all tend to place callbacks and errbacks at different positions in function signatures. Some don’t even accept an errback. Most don’t accept an alwaysback (i.e. “finally”). We can rely on the Promise API instead of many potentially different library APIs.
Third, a Promise makes a set of guarantees about how and when callbacks and errbacks will be called, and how return values and exceptions thrown by callbacks will be handled. In a non-Promise world, the multitude of callback-supporting libraries and their many function signatures also means a multitude of different behaviors:
- Are your callbacks allowed to return a value?
- If so, what happens to that value?
- Do all libraries allow your callback to throw an exception? If so, what happens to it? Is it silently eaten?
- If your callback does throw an exception, will your errback be called, or not?
… and so on …
So, while one way to think of Promises is as a standard API to callback registration, they also provide standard, predictable behavior for how and when a callback will be called, exception handling, etc.
What about try/catch/finally?
Now that we’ve restored call-and-return and removed callbacks from our function signatures, we need a way to handle failures. Ideally, we’d like to use try/catch/finally, or at least something that looks and acts just like it and works in the face of asynchrony.
In Part 3, we’ll put the final piece of the puzzle into place, and see how to model try/catch/finally using Promises.