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 .then
s and catch
es 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...
- Packages the Promise in a ripcord-ready state, but doesn't actually deploy the action, and...
- Magically combines all the chained functions wrapped in
then
,catch
, andfinally
intoresolve
andreject
(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()
orasync.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.