Skip to content

Instantly share code, notes, and snippets.

@domenic
Last active December 24, 2024 02:07
Show Gist options
  • Save domenic/3889970 to your computer and use it in GitHub Desktop.
Save domenic/3889970 to your computer and use it in GitHub Desktop.
You're Missing the Point of Promises

This article has been given a more permanent home on my blog. Also, since it was first written, the development of the Promises/A+ specification has made the original emphasis on Promises/A seem somewhat outdated.

You're Missing the Point of Promises

Promises are a software abstraction that makes working with asynchronous operations much more pleasant. In the most basic definition, your code will move from continuation-passing style:

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

to one where your functions return a value, called a promise, which represents the eventual results of that operation.

var promiseForTweets = getTweetsFor("domenic");

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.

I've talked about how cool I think promises are at length. This essay isn't about that. Instead, it's about a disturbing trend I am seeing in recent JavaScript libraries that have added promise support: they completely miss the point of promises.

Thenables and CommonJS Promises/A

When someone says "promise" in a JavaScript context, usually they mean—or at least think they mean—CommonJS Promises/A. This is one of the smallest "specs" I've seen. The meat of it is entirely about specifying the behavior of a single function, then:

A promise is defined as an object that has a function as the value for the property then:

then(fulfilledHandler, errorHandler, progressHandler)

Adds a fulfilledHandler, errorHandler, and progressHandler to be called for completion of a promise. The fulfilledHandler is called when the promise is fulfilled. The errorHandler is called when a promise fails. The progressHandler is called for progress events. All arguments are optional and non-function values are ignored. The progressHandler is not only an optional argument, but progress events are purely optional. Promise implementors are not required to ever call a progressHandler (the progressHandler may be ignored), this parameter exists so that implementors may call it if they have progress events to report.

This function should return a new promise that is fulfilled when the given fulfilledHandler or errorHandler callback is finished. This allows promise operations to be chained together. The value returned from the callback handler is the fulfillment value for the returned promise. If the callback throws an error, the returned promise will be moved to failed state.

People mostly understand the first paragraph. It boils down to callback aggregation. You use then to attach callbacks to a promise, whether for success or for errors (or even progress). When the promise transitions state—which is out of scope of this very small spec!—your callbacks will be called. This is pretty useful, I guess.

What people don't seem to notice is the second paragraph. Which is a shame, since it's the most important one.

What is the Point of Promises?

The thing is, promises are not about callback aggregation. That's a simple utility. Promises are about something much deeper, namely providing a direct correspondence between synchronous functions and asynchronous functions.

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

  • They return values
  • They throw exceptions

Both of these are essentially about composition. That is, you can feed the return value of one function straight into another, and keep doing this indefinitely. More importantly, if at any point that process fails, one function in the composition chain can throw an exception, which then bypasses all further compositional layers until it comes into the hands of someone who can handle it with a catch.

Now, in an asynchronous world, you can no longer return values: they simply aren't ready in time. Similarly, you can't throw exceptions, because nobody's there to catch them. So we descend into the so-called "callback hell," where composition of return values involves nested callbacks, and composition of errors involves passing them up the chain manually, and oh by the way you'd better never throw an exception or else you'll need to introduce something crazy like domains.

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

And, if you have a correctly implemented then function that follows Promises/A, then fulfillment and rejection will compose just like their synchronous counterparts, with fulfillments flowing up a compositional chain, but being interrupted at any time by a rejection that is only handled by someone who declares they are ready to handle it.

In other words, the following asynchronous code:

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

parallels* the synchronous code:

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

Note in particular how errors flowed from any step in the process to our catch handler, without explicit by-hand bubbling code. And with the upcoming ECMAScript 6 revision of JavaScript, plus some party tricks, the code becomes not only parallel but almost identical.

That Second Paragraph

All of this is essentially enabled by that second paragraph:

This function should return a new promise that is fulfilled when the given fulfilledHandler or errorHandler callback is finished. This allows promise operations to be chained together. The value returned from the callback handler is the fulfillment value for the returned promise. If the callback throws an error, the returned promise will be moved to failed state.

In other words, then is not a mechanism for attaching callbacks to an aggregate collection. It's a mechanism for applying a transformation to a promise, and yielding a new promise from that transformation.

This explains the crucial first phrase: "this function should return a new promise." Libraries like jQuery (before 1.8) don't do this: they simply mutate the state of the existing promise. That means if you give a promise out to multiple consumers, they can interfere with its state. To realize how ridiculous that is, consider the synchronous parallel: if you gave out a function's return value to two people, and one of them could somehow change it into a thrown exception! Indeed, Promises/A points this out explicitly:

Once a promise is fulfilled or failed, the promise's value MUST not be changed, just as a values in JavaScript, primitives and object identities, can not change (although objects themselves may always be mutable even if their identity isn't).

Now consider the last two sentences. They inform how this new promise is created. In short:

  • If either handler returns a value, the new promise is fulfilled with that value.
  • If either handler throws an exception, the new promise is rejected with that exception.

This breaks down into four scenarios, depending on the state of the promise. Here we give their synchronous parallels so you can see why it's crucially important to have semantics for all four:

  1. Fulfilled, fulfillment handler returns a value: simple functional transformation
  2. Fulfilled, fulfillment handler throws an exception: getting data, and throwing an exception in response to it
  3. Rejected, rejection handler returns a value: a catch clause got the error and handled it
  4. Rejected, rejection handler throws an exception: a catch clause got the error and re-threw it (or a new one)

Without these transformations being applied, you lose all the power of the synchronous/asynchronous parallel, and your so-called "promises" become simple callback aggregators. This is the problem with jQuery's current "promises": they only support scenario 1 above, omitting entirely support for scenarios 2–4. This was also the problem with Node.js 0.1's EventEmitter-based "promises" (which weren't even thenable).

Furthermore, note that by catching exceptions and transforming them into rejections, we take care of both intentional and unintentional exceptions, just like in sync code. That is, if you write aFunctionThatDoesNotExist() in either handler, your promise becomes rejected and that error will bubble up the chain to the nearest rejection handler just as if you had written throw new Error("bad data"). Look ma, no domains!

So What?

Maybe you're breathlessly taken by my inexorable logic and explanatory powers. More likely, you're asking yourself why this guy is raging so hard over some poorly-behaved libraries.

Here's the problem:

A promise is defined as an object that has a function as the value for the property then

As authors of Promises/A-consuming libraries, we would like to assume this statement to be true: that something that is "thenable" actually behaves as a Promises/A promise, with all the power that entails.

If you can make this assumption, you can write very extensive libraries that are entirely agnostic to the implementation of the promises they accept! Whether they be from Q, when.js, or even WinJS, you can use the simple composition rules of the Promises/A spec to build on promise behavior. For example, here's a generalized retry function that works with any Promises/A implementation.

Unfortunately, libraries like jQuery break this. This necessitates ugly hacks to detect the presence of objects masquerading as promises, and who call themselves in their API documentation promises, but aren't really Promises/A promises. If the consumers of your API start trying to pass you jQuery promises, you have two choices: fail in mysterious and hard-to-decipher ways when your compositional techniques fail, or fail up-front and block them from using your library entirely. This sucks.

The Way Forward

So this is why I want to avoid an unfortunate callback aggregator solution ending up in Ember. That's why I wrote this essay, with some prompting from @felixge for which I am thankful. And that's why, in the hours following writing this essay, I worked up a general Promises/A compliance suite that we can all use to get on the same page in the future.

For example, at current time of writing, the latest jQuery version is 1.8.2, and its promises implementation is completely broken with regard to the error handling semantics. Hopefully, with the above explanation to set the stage and the test suite in place, this problem can be corrected in jQuery 2.0.

Since the release of that test suite, we've already seen one library, @wycats's rsvp.js, be released with the explicit goal of providing these features of Promises/A. I'm hopeful others will follow suit. In the meantime, here are the libraries that pass the test suite, and that I can unreservedly recommend:

  • Q by @kriskowal and myself: a full-featured promise library with a large, powerful API surface, adapters for Node.js, progress support, and preliminary support for long stack traces.
  • rsvp.js by @wycats: a very small and lightweight, but still fully compliant, promise library.
  • when.js by @briancavalier: an intermediate library with utilities for managing collections of eventual tasks, as well as support for both progress and cancellation. Does not guarantee asynchronous resolution.

If you are stuck with a crippled "promise" from a library like jQuery, I recommend using one of the above libraries' assimilation utilities (usually under the name when) to convert to a real promise as soon as possible. For example:

var promise = Q.when($.get("https://github.com/kriskowal/q"));
// aaaah, much better
@Marco-Sulla
Copy link

Hello. Promises seems very cool and it's a pity that jQuery implements them wrongly. What about Bluebird?

@mgol
Copy link

mgol commented Mar 20, 2016

For anyone that might observe this thread - jQuery will have Promises/A+-compatible implementation in version 3.0.0 that should be released soon; you can test the beta at https://code.jquery.com/jquery-3.0.0-beta1.js or [email protected] if you install via npm.

@guest271314
Copy link

@mgol How the tests be performed on jQuery version 3.0.0-beta1.js in the browser without using nodejs?

@keithdtyler
Copy link

keithdtyler commented May 16, 2016

Sometimes you want to proceed with actions after a promise is done. And you want to take the results of the promise, build upon them, and proceed with the result. In the functional world you say:

function bob() { <do a thing>, return "something" }
function fred(thing) { <do another thing> }

var result = bob();
fred(result);

If bob is a promise though, instead of that natural linear progression, you have no choice but to do something like this;

var result = bob() {a promise}
result.then(function(it) {fred(it);})

But what if fred also returns a value, and you want to do something else with that value? Now you're stuck doing this:

result.then(function(it) {var newvar = fred(it); newvar=newvar+12; if (newvar>20) { killCrites(newvar); } );

Seriously...

Basically, you end up creating bubble universes of functionality, which cannot speak to each other and cannot interact with the outside world. You can easily end up with multiple rabbit holes going on.

Sometimes you just want to wait until the damned promise is resolved and then get its result as a tangible thing (not, that is, a promise) and then proceed.

@muxahuk
Copy link

muxahuk commented May 31, 2016

@keithdtyler
you could write your example like these:

var result = bob()
.then( fred ) // pass function name only
.then( ( newvar ) => { // newvar returned from fred function
    newvar += 12;
    if( newvar <= 20 ) throw new Error( 'newvar less then 20' );
    return newvar;
} )
.then( killCrites ) // killCrites called only if newvar > 20
.catch( error => {} ); // On Error thrown previosly

Aldought it does the same as you wrote it - it's more functional-programming-alike style
As u can see u can pass just function name inside .then, but it gets tricky when you need to pass several parametrs to a function..

@dy
Copy link

dy commented Apr 22, 2017

I imagine a case when some thenable object is "fulfilled" with value that can be "fulfilled" more.
Take for example audio. We may think of making it thenable:

let audio = new Audio('./src.mp3')
    .then(audio => audio.insert('./src2.mp3'))
    .then(audio => audio.process(handler))

In this case in the first callback audio is fulfilled with './src.mp3' contents, but then we want to load and insert new content via audio.insert('./src2.mp3').

We could make audio.insert('./src2.mp3') return a new audio with only the inserted and decoded fragment ./src2.mp3, but then next chain callback would get only that src2 part, not the full one with content of both sources, so that's a no-go.

We could make audio load/record content only once, ie be a valid promise and don't let inserting async content:

new Audio('./src.mp3')
    .then(audio =>
        new Audio('./src2.mp3')
        .then(audio2 => audio.insert(audio2))
    )
    .then(audio => audio.process(handler))

But that looks less elegant and forbids updating like audio.insert('./src').then(audio => {}, err => {})

One other solution is pouchDB API − in case if there is a callback as the last argument, it returns instance. If there is no callback - it returns a promise.
The downside is that the behavior of a function alters depending on the arguments, which might be confusing. Is the created audio a promise or audio instance - no obvious answer.

The other solution - having multipromise, a promise that can be resolved multiple times, each time for every insert call. But that brings us to the initial problem. Also that is non standard-compliant.

Yet another solution is returning new audio handle for the same data every insert call. But the code may turn into not understandable mess pretty soon.

Finally we can not use promises. Who needs that can do that via any promisify library. Related video: https://www.youtube.com/watch?v=GaqxIMLLOu8

@AdrienHorgnies
Copy link

AdrienHorgnies commented Jul 31, 2017

Thanks for the article. I was having a nightmare of making two libraries work together, one written in promises, inquirer.js and one written in callbacks, mysql.js. I thus concluded that there was something I didn't get about promises and ending up reading this article of yours plus many others.

I understood a thing or two, mainly that I don't like promises :-s. And I'm still wondering why inquirer.js, a library to prompt the user, is written with promises when you must wait the user input anyway...

And as others, I really think promises is not the answer to all that async stuff, it's making me loose so much time.

I've promisified some mysql.js functions myself, because their callbacks weren't anything standard (or at least I failed to use libraries such as bluebird or util on it) and asked my senior how I could have something cleaner and he answered me "Let the monster be".

@saintsGrad15
Copy link

Just a thought...

I don't deny the thesis of this article but there is at least one other use case for Promises.

My use case (in Browser JS, for what it's worth) is not chaining/composition. It is a case where I want several asynchronous things to happen (AJAX calls in all cases). I kick them all off ASAP as the program loads. None of them are dependent upon each other.

Throughout the rest of my code various things depend upon one or more of these asynchronous things completing, usually successfully. These dependent "things" maybe be chomping at the bit waiting for the async to complete or they may not happen until a particular user interaction takes place later, if at all.

In this case, each async action is independent and the things that depend upon one or more of the async actions are not, themselves, async.

So, I don't refute the point @domenic is making but simply want to acknowledge this other use for Promises.

I am highly open to an alternative native solution to this use case but I assume that if any such thing exists it would look a lot like Promises but without the "chainability." Such an implementation seems unnecessary given that Promises case accomplish this just fine and rather elegantly.

@Swivelgames
Copy link

I'd like to point at that, depending on the environment you're developing in, Promise/A+ have become a part of the ECMAScript language specification. So whether it be Node.js or JavaScript, Promises are first-class citizens. As they become more and more common, I think their utility, convenience, and usability will become a bit more widely-known.

Personally, as a Team Lead helping to educate his team members, it's become quite apparent that one of the larger problems is the esoteric-nature of Promise's and the biggest problem seems to be the lack of understanding of when, and especially how, to use them efficiently. Once efficient examples are shown, it's amazing how quickly Promises start popping up in code; not for the sake of using Promises, but in areas that improve efficiency and maintainability.

@saintsGrad15 brought up another great use-case and utility for Promises that are often over-looked. Promises can allow virtually any asynchronous operation's results and failures to be come predictable and intuitive. By adapting existing code to expect Promises, and by modifying asynchronous functions to use Promises as their return signature, it's easy to adapt applications to become incredibly efficient.

@Swivelgames
Copy link

Also, @dy, I see one of your examples as a common theme. Most people jump to nest Promises because it's very natural based on the old callback-style of coding. But this might be more elegant, readable, and efficient, while maintaining the functionality:

Promise.all([
  new Audio('./src.mp3'),
  new Audio('./src2.mp3')
])
  .then(([audio1, audio2]) => {
    audio1.insert(audio2));
    return audio1;
  })
  .then(audio1 => audio1.process(handler))

Or, if new Audio('./src.mp3') must happen before the instantiation of Audio with src2.mp3:

Promise.resolve(new Audio('./src.mp3'))
  .then(audio => Promise.all(audio, new Audio('./src2.mp3')))
  .then(([audio1, audio2]) => {
    audio1.insert(audio2));
    return audio1;
  })
  .then(audio1 => audio1.process(handler))

@rakesh-3607
Copy link

Is Null a legitimate return type of Promise ?

@chase-moskal
Copy link

what a great little piece of javascript history, this article and thread

of course, promises are now a cherished feature of javascript, and combined with async/await syntax, promises have "solved" javascript's "async problem" to the satisfaction of the community at large

🍾 🎉

it would be interesting to hear what the various naysayers have to say, all these years later ;)

@rakesh-3607 -- you betchya, null is a valid promised value

@Guseyn
Copy link

Guseyn commented Apr 11, 2019

Promises and async/await abstractions suck, declarative programming rules. Just use this lib to get rid of pain.

@Prinsn
Copy link

Prinsn commented Aug 14, 2019

of course, promises are now a cherished feature of javascript, and combined with async/await syntax, promises have "solved" javascript's "async problem" to the satisfaction of the community at large

Yeah, except when you get a library or client that only has asynchronous access to values you need synchronously, so that you keep promise chaining until you get an infinite loop in your framework and it's literally the best solution to hack something together to get around it than invest in the lost time trying to migrate to some other feature or debug the framework.

@Richardinho
Copy link

This article has been given a more permanent home on my blog

that link is broken now, lol

@domenic
Copy link
Author

domenic commented Mar 4, 2023

that link is broken now, lol

Fixed; thank you!

@mgol
Copy link

mgol commented Mar 4, 2023

It’s be great if you updated the header on that blog post. It continues to strongly claim latest jQuery is not compliant and that it will never be whereas jQuery has been compliant since 3.0.0, released in 2016.

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