I'm not sure why, but Promises in JavaScript took a while for me to grok. I mean, the premise is simple: You have a function, and once it's done, whenever that is, the appropriate chained methods will be executed, like .then if it's successful and .catch if it isn't, with the appropriate parameters given. That's easy, and you can start using Promise workflows quickly.

But how do you make a Promise? Strangely, many tutorials approach this as if all Promises were sui generis (sui generis apparently means "of its own kind" -- as in something that can't be reduced to something simpler. Or, as the Wikipedia might put it, "[E]xamples are sui generis [when] they simply exist in society and are widely accepted without thoughts of where they come from or how they were created").

Let's correct that approach. MDN's most basic example of creating a Promise is pretty straightforward:

const myFirstPromise = new Promise((resolve, reject) => {
  // do something asynchronous which eventually calls either:
  //
  //   resolve(someValue); // fulfilled
  // or
  //   reject("failure reason"); // rejected
});

Do something, then resolve or reject it. That's simple. We're used to doing just that in callback-land excepting the Promise wrapper.

Take this example code from a pretty good, very basic tutorial on Promises from Wesley Handy here.

function getData() {
    return new Promise((resolve, reject)=>{
        $.ajax({
            url: `http://www.omdbapi.com/?t=The+Matrix`,
            method: 'GET'
        }).done((response)=>{
                //this means my api call suceeded, so I will call resolve on the response
                resolve(response);
        }).fail((error)=>{
                //this means the api call failed, so I will call reject on the error
                reject(error);
        });
    });
}

The behind the curtain magic happens in the formulation of resolve and reject. You set those up with this:

getData()
    .then(data => console.log(data))
    .catch(error => console.log(error));

If you're not careful, your Promise tutorial might accept that that's magic, and not bother asking Mitch Pileggi how the trick was done. What you're doing is wrapping all of your .thens and catches into two [possibly composite] functions. This is that "syntactic sugar" everyone loves to talk about.

Unraveling a Promise

The quick getData call, above, looks like this outside of the Promise-sugar.

function doIt(resolve, reject) {
    $.ajax({
        url: `http://www.omdbapi.com/?t=The+Matrix`,
        method: 'GET'
    }).done((response)=>{
        //this means my api call succeeded, so I will call resolve on the response
        resolve(response);
    }).fail((error)=>{
        //this means the api call failed, so I will call reject on the error
        reject(error);
    });
}


var ajaxSuccess = function (data) {
    console.log(data);
}

var ajaxFail = function (error) {
    console.log(error);
}

doIt(ajaxSuccess, ajaxFail);

That's it. That's what that call looks like outside of a Promise. There's no real Promise-centric benefit for this simple case, imo.


Unraveling a Promise with a finally

More interesting would be if that example also had a finally, like this:

getData()
    .then(data => console.log(data))
    .catch(error => console.log(error))
    .finally(() => console.log("Finally gets no arguments"));

That looks like this, with the same doIt function as earlier (ie, doIt doesn't change):

function doIt(resolve, reject) {
    $.ajax({
        // Same as above...
}

// Here's the new function we want to use after `doIt` completes, no matter what.
var ajaxFinally = function () {
    console.log("Finally gets no arguments");
}

// Now back to our success/fail functions with *one* change for each...
var ajaxSuccess = function (data) {
    console.log(data);
    ajaxFinally();    // and now we add it to BOTH the success and fail functions.
}

var ajaxFail = function (error) {
    console.log(error);
    ajaxFinally();    // <<< OMGWTFBBQ!!1! ajaxFinally is here, too.
}

doIt(ajaxSuccess, ajaxFail);

Here, we do get a little bit of a readability improvement, and certainly some DRYness.


Promise wrapping

That is, the Promise wrapper...

  1. Packages the Promise in a ripcord-ready state, but doesn't actually deploy the action, and...
  2. Magically combines all the chained functions wrapped in then, catch, and finally into resolve and reject (more precisely, "into the two parameters any Promise expects"), above.

Again, the Promise constructor just does some sugar-magic to wrap up all the chained functions that follow it once its async action completes, and route logic into one (resolve) or the other (reject) once the wrapped action is complete. Fwiw, all the chainable functions are described here, at MDN.

I think part of my block on Promises was that callbacks are so danged easy to understand. And if you have a true pyramid of callback doom, I've taken that as a code smell rather than an insurmountable issue inherent to javascript. What is this Promise doing that makes it simpler to follow than callbacks? In a sense, nothing. It's hiding how callbacks are chained together. Some folks find functions-as-objects difficult to grok, and I think the structure Promises gives helps folks that don't like function-freewheeling.

But, as thecodebarbarian.com explorer here, pyramids usually are signs of bad architecture, not a place where you're limited by code.

This case [of "callback hell" used as an example earlier] is a classic example where the single function is responsible for doing way too much, otherwise known as theย God object anti-pattern. As written, this function does a lot of tangentially related tasks:

  • registers a job in a work queue
  • cleans up the job queue
  • reads and writes from S3 with hard-coded options
  • executes two shell commands
  • etc. etc.

The function does way too much and has way too many points of failure. Furthermore, it skips error checks for many of these. Promises and async have mechanisms to help you check for errors in a more concise way, but odds are, if you're the type of person who ignores errors in callbacks, you'll also ignore them even if you're usingย promise.catch()ย orย async.waterfall(). Callback hell is the least of this function's problems. [emph mine, natch -mfn]


TL;DR

I'm belaboring the point, but bottom line is very simple:

Promises simply use chaining functions to organize your callbacks into two composite functions. One composite function gets called on success, one on failure.

here endeth the lesson

Labels: , ,