Skip to content

Instantly share code, notes, and snippets.

@david-mark
Last active January 10, 2017 05:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save david-mark/1a0f8ba00d7514a3946938b02fafa1a3 to your computer and use it in GitHub Desktop.
Save david-mark/1a0f8ba00d7514a3946938b02fafa1a3 to your computer and use it in GitHub Desktop.
Promises, Promises

Always dubious on articles that open with "You're missing the point of..." as they so often demonstrate only that the author has missed a trick (or two or three).

It starts out with a note that one of the most popular implementations of Promises is (and will likely remain) broken:

Contrary to some mistaken statements on the internet, the problems with jQuery’s promises explained here are not fixed in recent versions; as of 2.1 beta 1 they have all the same problems outlined here, and according to one jQuery core team member, they will forever remain broken, in the name of backward compatibility.

Have heard rumors that this is fixed in jQuery 3.0, but jQuery should be a dead issue by now. Perpetually updating a long since irrelevant (and also non-standard) implementation of the browser-provided Selectors API is a non-starter. Using such a thing for a tacked-on Promises implementation can only be attributed to some sort of misguided brand loyalty. Queries are just not asynchronous. Ajax is (or should be) used in asynchronous fashion, but that's also a tacked on jQuery feature. Highly recommend getting off the jQuery train at this point, but I digress.

The (ahem) promise of Promises is spelled out as:

Promises are a software abstraction that makes working with asynchronous operations much more pleasant.

So it's the software equivalent to a piece of grit they put in to make the experience... more pleasant? :)

As often seen in marketing, we start with the before picture:

getTweetsFor("domenic", function (err, results) {
  // the rest of your code goes here.
});

Can assume that the getTweetsFor function makes an asynchronous Ajax request. Okay, but the example is odd for a couple of reasons.

Like all asynchronous operations, Ajax requests are best handled by object-oriented code, something oddly ignored throughout this piece. Perhaps because it illuminates the fact that Promises are an unneeded commodity? We simply construct an object with callback methods, much like the odd object that functions using Promises return.

The second issue is apparent from the odd function signature. Has two arguments: err and results; the former is obfuscating shorthand for "error" and the latter will reference the response to the request. Assume these arguments are mutually exclusive, which allows for only two outcomes; however, an Ajax response (like most asynchronous operations) has three possible outcomes:

  1. Successful response
  2. Failed response (e.g. HTTP errors)
  3. No response

The third case represents a server time-out (not to be confused with a 408 status, which indicates the client was too slow to provide the request). Can also indicate the case where the client explicitly canceled the request (which should not result in any callback). The former case is covered neither by the provided example nor by Promises. This is a glaring omission and can confidently stop reading the explanation (and this rebuttal) at this point.

For the uninitiated, and before delving into the implications for Ajax and browser scripting in general, consider the example of a telephone pollster soliciting answers to yes/no questions. For each number they call, there are three possible outcomes:

  1. Yes
  2. No
  3. No response

The third case arises when there is no answer (or perhaps the sitter answered as the occupants were out).

The parallels to Ajax requests should be clear: requests can time out. The standard XHR implementation treats the third case like the second, but with a special result code to indicate that the result is indeterminate. Any Ajax library worth its salt should simplify such confusion (i.e. make our code more "pleasant") by providing three possible callbacks. This is particularly trivial in the common case where the time-out is managed by the library; instead of relying entirely on the browser's idea of too long a wait, libraries often set their own time-out functions to cut the request short if a response is not received within n seconds.

For example, we make a request to a server to add a comment. The server will take an indeterminate amount of time to add the message to a database and return a response. If the request is cut short, there's no useful status code (or no status code at all) to indicate what happened (e.g. 500 to indicate a server error, 403 to indicate an unauthorized action, 422 for invalid data sent, etc.) An example of a less than useful status code is 504, which indicates the server got tired of waiting for some other server to respond.

In any event, the result is indeterminate and should normally result in a retry/cancel dialog presented to the user. The user may immediately try again and the cycle will continue until such time as a definitive status code is received. They may find that the comment was added after all and the subsequent request would result in a duplicate (code 422) or the comment may be posted without issue (code 200) as the previous attempt failed to finish. Alternatively, they may just decide to cancel and try again later (perhaps the server was just too bogged down at the moment).

Let's segue to modal dialogs as they represent another common browser scripting task that requires asynchronous handling. Take a yes/no/cancel dialog for example. Again, there are three possible outcomes, which an ideally "pleasant" API will route to three different callbacks:

  1. User agrees
  2. User disagrees
  3. User is undecided

See the pattern? More importantly, should see how the indeterminate cases are not covered by Promises. They can be shoehorned into Promises (as they often are in Ajax libraries) with special result codes to indicate an indeterminate result, but how is that supposed to make our coding more "pleasant". Perhaps elegant is a better word? In any event, it doesn't make it.

That is the second clue to stop reading, but there is more to dissect here. The "after" picture looks like this:

var promiseForTweets = getTweetsFor("domenic");

So instead of constructing an object to manage requests and responses related to Tweets, we should call a function that constructs a new object each time, juggle it for a bit and then discard it? Doesn't seem like an elegant (or efficient) approach at all. This is how jQuery does everything, including Ajax, and it's always been a hard way to go. The rationalization for Promises is stated as:

This is powerful since you can now treat these promises as first-class objects, passing them around, aggregating them, and so on, instead of inserting dummy callbacks that tie together other callbacks in order to do the same.

...but seems more like comparing two rotten apples. As mentioned, we could have constructed our own reusable first-class object (all javascript objects are first-class) with appropriate callback methods. There would be no need to pass the callbacks around, but could easily aggregate or chain them together by overriding these methods. As for "dummy callbacks", it's anyone's guess what that means. Certainly there are no examples included to illuminate these claims. There is this admission:

I’ve talked about how cool I think promises are at length.

...which links to an apparently lengthy article expounding on the "coolness" of Promises. Perhaps there are some relevant examples there, but it's very hard to imagine any pitch that will seal this deal. Readers are encouraged to look if still interested at this point. Seems like the third sign to stop reading, but there are a couple more code examples to consider.

After some talk about library implementations of Promises, under the heading of "What Is the Point of Promises?" there is an oddly different rationalization based on synchronous vs. asynchronous code. Assume the former means functions that do not have callback arguments (unlike the first example) and neither return an object for us to augment with callback functions. In the case of modal dialogs, this would be a non-starter. For Ajax, it would require synchronous requests, which have no place in a competently designed application (e.g. they lock up the browser window until a response or time-out). Unfortunately, this last comparison is another variation on Ajax requests related to Tweets.

Here is the setup:

What does this mean? Well, there are two very important aspects of synchronous functions:

They return values They throw exceptions

Some functions return values, whether they meet the assumed definition of asynchronous or not. For example, a function that makes an Ajax request and uses callback arguments may return a boolean value to indicate that the request never got off the ground (e.g. same origin policy violation). Or they may throw an exception in that case as it would likely be the sort of thing that happens only during debugging.

The point of promises is to give us back functional composition and error bubbling in the async world. They do this by saying that your functions should return a promise, which can do one of two things:

Become fulfilled by a value Become rejected with an exception

Never mind the technobabble about functional composition and error bubbling. Once again, the problem here is the number of outcomes; in this case, two out of three is bad.

Also note that the second case seems to indicate that an exception unrelated to debugging will be thrown and caught at some point. That's very bad news for our debugging efforts. If a debugger is a cop, we want it to catch the criminals (bugs) in the act. The code should break and provide an opportunity to inspect the scene (variables, stack trace, etc.) before it becomes contaminated by the code moving forward. The alternative when catching unexpected exceptions is to file a report (log the details) to be reviewed later.

In short, we shouldn't normally wrap our own code in try-catch statements as it makes it much harder to debug. An exception to this rule (no pun intended) is when we expect that a host object may throw an exception (e.g. during initial feature testing). Furthermore, up until recent revisions, exception handling in javascript has always sucked. Unless our code is to run exclusively in environments that support the much-needed improvements to exception handling, it's virtually impossible to figure out exactly what caused the exception to be thrown. For this reason, try-catch statements are traditionally used to wrap a single line of code with clear-cut expectations for how it may fail (e.g. trying to construct an ActiveX object when ActiveX is disabled).

The only point of explanation made about throwing exceptions is:

and oh by the way you’d better never throw an exception or else you’ll need to introduce something crazy like domains.

...which links to an article about the (apparently soon to be deprecated) Domain module for NodeJS processes. This is clearly irrelevant to browser scripting where exceptions are virtually always thrown for debugging purposes and there are no processes to shut down as a result. All bets are off when browser scripts throw exceptions in production, which is why ease of debugging is so important.

Have seen misguided attempts to implement global error handling in browsers, but the only reasonable action on catching an exception would be to try to prompt the user to reload the page. As we wouldn't know exactly what led to the exception, we couldn't know whether such a prompt may throw another (though perhaps calling window.confirm, followed if confirmed by window.location.reload would be the closest thing to a safe option at that point). But I digress.

Here is the "asynchronous" example:

getTweetsFor("domenic") // promise-returning function
  .then(function (tweets) {
    var shortUrls = parseTweetsForUrls(tweets);
    var mostRecentShortUrl = shortUrls[0];
    return expandUrlUsingTwitterApi(mostRecentShortUrl); // promise-returning function
  })
  .then(httpGet) // promise-returning function
  .then(
    function (responseBody) {
      console.log("Most recent link text:", responseBody);
    },
    function (error) {
      console.error("Error with the twitterverse:", error);
    }
  );

From the previous explanation, it seems clear that the "promise-returning function" calls are going to be wrapped in try-catch statements. But regardless, the above can easily be refactored by composing functions. The benefit is that there would then be no need for a complicated, behind-the-scenes chaining library to add hidden complexity to the code (see also: jQuery, D3, etc.) Simpler is virtually always better (i.e. more elegant and "pleasant" to follow).

Mercifully, here is the final example representing "synchronous" code:

try {
  var tweets = getTweetsFor("domenic"); // blocking
  var shortUrls = parseTweetsForUrls(tweets);
  var mostRecentShortUrl = shortUrls[0];
  var responseBody = httpGet(expandUrlUsingTwitterApi(mostRecentShortUrl)); // blocking x 2
  console.log("Most recent link text:", responseBody);
} catch (error) {
  console.error("Error with the twitterverse: ", error);
}

Not only does it imply synchronous requests, but it wraps three different function calls (plus an assignment and host object method call) in a single try-catch. As expected, the exception handling code simply files a report for later review.

Once again, this isn't remotely close to how it's done, so the comparison is ultimately useless.

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