Using JavaScript Promises to Reason About User Interaction

In functional programming, we rarely worry about concurrency primitives like
threads. Instead, we use abstractions to make our code easier to reason about.
In JavaScript, a Promise is the most common abstraction we encounter.
It is usually associated with an Ajax request. We often use abstractions to
reason about our concurrent code, but those abstractions can apply in other
situations, as well. High level user interactions are essentially large
asynchronous events, and can be represented as such in code.

Let’s take a look at an example. We’re working on a video store front. The user
can buy videos, and play videos that they own. If they aren’t signed in, they
need to sign in to buy a video, and we’d like to use modal dialogs for the sign
in and purchasing process.

In order to determine if a user already owns the content, we need them to be
signed in. It’s also possible that upon signing in, they could already own the
content they tried to purchase. This can quickly lead to an extremely complex
set of conditionals, but we can simplify it by thinking about the problem like
we would think about concurrency.

Any time we’re dealing with user interaction, it is inherently asynchronous.
In most code, this begins and ends with event handlers on individual DOM
elements. This can be better abstracted if we think about the larger
interactions a user can take, and represent that as a single asynchronous
operation in the code.

A user may or may not be signed in. We could expose that, and let that
conditional bubble throughout our code. However, most of the time, if a user
isn’t signed in, we’re just going to prompt them to sign in. That means that
rather than having a user who may or may not be signed in, we’ve always got a
user that will be signed in at some point in the future. Let’s look at how we
might represent this in code:

There’s a lot going on here. Q is Kris Kowal’s Q library for working
with promises. We’ll gloss over the implementation of AuthModals, and
Session. We’ll just assume that AuthModals opens the modal dialog, and
something will call completeSignIn when the form is submitted. Session does
two things. It asks the server if we’re already signed in, and will submit the
Ajax request to the server, and return a promise with the session object.

When code calls the signIn method, we’re returning a promise that will
eventually be resolved with the current user. If they’re already signed in, we
resolve that promise immediately. Otherwise, we open the modal dialog for the
user to sign in, and resolve the promise once the form is submitted.

This is incredibly powerful. We’ve completely removed the need for code to
worry about whether or not the user is signed in for almost all cases. We can
build upon this further by applying the same concept to purchases.

We apply the same pattern here. If a user already owns the video, we resolve
the promise with the license immediately. If not, we prompt the user to buy it,
and resolve it when the license is given to us later. A user can only buy one
thing at a time. If the user closes the modal and tries to buy something else, the
old promise is effectively thrown away.

Finally, we can hide all of this away inside of an object responsible for
playing the video.

With this in place, code that wants to play a video needs only to call
VideoPlayer.play. It doesn’t need to concern itself with whether or not the
user is signed in, or whether they already own the content. We would still need
to have a conditional inside of the template to decide whether the button
should say ‘Buy’ or ‘Play’. However, if we use the Null Object
pattern, and give the template a Guest when the user isn’t
signed in, this still remains relatively simple.

By thinking about higher level user interactions the same way we would think
about a larger asynchronous computation, we’ve avoided littering our code with
ugly conditionals, and are able to isolate concerns like login and
purchasing content to a single place. Testing each of these objects
individually also becomes much easier than having to deal with the four
cases in one place.

These kinds of situations are everywhere if you look for them. You just have
to remember that asynchronicity isn’t just for concurrency, and promises aren’t
just for XHR.