Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Why are callbacks more “tightly coupled” than promises? (stackoverflow.com)
65 points by rdfi on March 7, 2014 | hide | past | favorite | 34 comments


This is not a very good or complete answer as others are pointing out.

Promises allow you to make your async call in a context where you don't know what has to be done next. You pass the promise around and attach to it as needed (and chained if necessary), that is the lack of coupling that is so desirable.


You can technically achieve this from callbacks as well. Simply pass in the callback as a function to the method. This way you can still make your async call in a context where you don't know what has to be done next - what will be done next can be passed in dynamically to the function.

That said, I still much prefer promises as they make it much more explicit.


I'm confused by your comment.

> Simply pass in the callback as a function to the method. This way you can still make your async call in a context where you don't know what has to be done next - what will be done next can be passed in dynamically to the function.

That sounds like you're just describing how callbacks work in general... The point is that you have to know what function to pass in at the time you make the asynchronous call. With promises, you can figure that out later.


Let's say I'm in the middle of the method foo, and I do some asynchronous operation.

With a promise, I can do two things, I can pass it into some other function, or I can return it to the caller of foo

with a callback, I can do two things, I can supply a callback which passes the result into some function, or I can require that the caller of foo provide a callback, which I pass along to the asynchronous operation I'm calling

these seem roughly isomorphic to me -- I guess a difference is that if I pass the promise to a method I'm allowing the method to decide when to deref it, but that seems different from the "tightly coupled" claim.


It's a matter of timing. A callback function could be called immediately and you need to be ready for that, so you need to define the callback before passing it in. With a promise, you can call the then() method after the function returns; if the value is ready earlier then it will automatically be cached until you pick it up. The looser requirements on timing result in better decoupling; like any other value, you can put the promise in an arbitrary data structure and pick it up any time later, or perhaps not at all.

If you want to do this with callbacks you'll need to implement a way to cache the value and the result will be equivalent to a promise.


Returning a promise to the caller and requiring the caller to pass in a callback are not isomorphic. If you return a value to me, I can figure out what to do with it whenever I want. If you make me pass you a callback, I have to know what I want to do, right when I call you.


There is a slight limitation where you async operation returns before you assign your call back.

By using an object (in this case a promise) you could cache the result and make it available to anyone adding a listener in the future.


And that is what I pointed out in the answer, although worded a little differently: "If you use a callback, at the time of the invocation of the asynchronous operation you have to specify how it will be handled, hence the coupling. With promises you can specify how it will be handled later."


But... when that "later" is literally the next clause, I'm not sure the wins.

It also gets dangerously close to the reported virtues of try/catch. Since you don't have to specify "how errors are handled until later." Seems the sooner you specify some things the better.


And why does that happen?

I think the answer is unfortunately simple: callback are typically written inline, and promises are written as separate named functions that are wired up after the fact. You can do whatever you want with both, but the way jQuery implements both encourages better code from promises than callbacks.


As I mentioned in the comments, those examples aren't actually equivalent, the callback version could be:

    var getData = function(callback){
        showLoadingScreen();
        $.ajax("http://someurl.com", {
            complete: function(data){
                hideLoadingScreen();
                callback(data);
            }
        });		
    };

    var loadData = function(){
        getData(doSomethingWithTheData);
    }

    var doSomethingWithTheData = function(data){
        //do something with data
    };


Exactly. This is the problem I have with the hype around the promises API, and if you're a Dart developer, Futures.

They claim to solve "callback hell" but they can't. Callback hell is the result of poor programming style, not a limitation of the language. Callback hell is just as possible with promises, and there are plenty of examples of it on the internet.

I'm not saying promises don't have benefits, and can't result in some nice programming patterns, but generally they are just a different way of doing the same thing. It's not that callbacks are more tightly couples then promises, its that people write their code that way.

Their API's have some other nice benefits though.


I think Callbacks, Promises and Futures are all more or less the same in terms of coupling. However, I do find Futures to produce the most readable code.


I would agree. I've found, in my own code at least, that callback hell only happens with code that is not modular enough. If you separate your logic out into small self-contained modules that do one thing, they never get too deep. Promises allow you to have monolithic code that's not superficially ugly.


Yup. I actually adore promises, but I don't buy the decoupling argument at all. And the thing I keep seeing about how promises let you decide what to do with the data after you make the call as opposed to before seems...totally silly, since I had to make that decision when I wrote the code.


When people say "later", they don't mean in human time, they mean in program execution time. As in, your program can decide based on the dynamic state of the system how (or even whether) to use the resolved value. In my experience, that is not nearly as convenient to do with callbacks as it is with promises.


Later in that sense isn't usually very useful, since if the promise isn't resolved, there's no new information about the state of the system. Where I think promises help is that they allow to express the concept of "do stuff later" in a cleaner, more extensible way. It's really just about having a nicer interface (you can even ask it questions like, "what's been done?"). But the part where you decide what the promise will do after you specify what it's a promise for isn't, in my view, an important detail.


I think we actually agree. Deciding "later" to do nothing is exactly the type of decision I'm talking about.


ember-data is a great example of why promises can be useful. When a view is loaded you query your server for the model(via ember-data's api), but at this point what you get is a graph of unresolved promises. It then gets passed through to the controllers/templates etc. which render immediately but are then updated automatically as soon as the promises resolve. This is the real value of promises, you can pass them around your app before they're resolved.


(Note: I haven't worked with Ember, and this probably doesn't jive with how they do things)

This sounds a lot like traditional async Model/View programming. I've traditionally passed a model around, and use subscriptions to listen for "data." That has the added benefit of being reusable, with the model dispatching change events. Promises in this case sound like they are fulfilling a similar role by your description.


Yep, doing much the same thing in angularjs as we speak. It works very nicely and would have been a pain with only callbacks.


Thinking about this from a Haskell perspective, it seems that with promises the lambda waiting for the callback is basically encapsulated into the promise:

  type Promise a = (a -> IO ()) -> IO ()
  getData :: Promise String
Whereas with callbacks you pass that around explicitly:

  getData :: (String -> IO ()) -> IO ()
Promises are a monad, so you get the usual monad advantages of being able to "hide the plumbing" behind the abstraction, hence no tower of explicit callbacks in your JS code.

  instance Monad Promise where 
    return x = \k -> k x
    fmap f p = \k -> p (k . f)
    mx >>= fxmy = \k -> mx (\x -> fxmy x k)


Thinking about this from a Haskell perspective isn't very helpful, because what you actually get is simply an "IO String", and it doesn't behave much like any Node option. If you want concurrency, you use forkIO, or one of the many things built on top of it (async, concurrent pipes, whatever), and if you don't, you don't. Regardless, the IO type is always "async" in the way that Node types define it (or, alternatively, transcends the way Node uses the term entirely); there is no distinction between an IO String that is simply 'return "a string"' and an IO String that actually hits a web server API to figure out what other web server to hit for a file which will then be fed to an API which will return a string... that's still just an "IO String".

Haskell does have a thing from the promise tradition, but it's a different definition of "promise" than JS is using here, and it manifests in Control.Concurrent.MVar. (And, again, let me be clear, if that doesn't look like what the JS is doing here, correct. The wikipedia page is helpful. [1] AFAIK, JS is not wrong in its usage of the term promise, but there's a lot of other academic study behind the term and there's a lot of different variants in that history.)

You don't need a promises monad, IO already does it, combined with the scheduler.

[1]: http://en.wikipedia.org/wiki/Futures_and_promises


> Promises are a monad,

Actually, they're not. Get ready: https://github.com/promises-aplus/promises-spec/issues/94


Promise A+ is scary bad. They're trying to shoehorn all of the functionality of a monad based implementation into a single method: http://promises-aplus.github.io/promises-spec/#the__method

Of course, this relies on a lot of dynamic inspection and runtime checks. Of course, this means that you can't compose with A+ promise methods in a reliable way. Of course, this means that their implementation is not going to be compatible with anyone else's. Of course, this means that everyone else's version of promises is "broken".

If you're interested in approaches that follow algebraic/monad patterns, check out fantasyland spec: https://github.com/fantasyland/fantasy-land

The name of the spec is a joke, but the spec itself is not. It's referencing a very real sentiment: That developers that want to use formal composition techniques in their js are living in a "fantasy land".


I like harpo's answer better than the accepted answer. I'll help generate more upvote for that answer from here :)

> The coupling is looser with promises because the operation doesn't have to "know" how it continues, it only has to know when it is ready.

> When you use callbacks, the asynchronous operation actually has a reference to its continuation, which is not its business.

> With promises, you can easily create an expression over an asynchronous operation before you even decide how it's going to resolve.

> So promises help separate the concerns of chaining events versus doing the actual work.


Here's a contrived example:

Promises:

  //model
  User.get = function() {
    return getData();
  };
  User.getOdd = function() {
    return User.get().then(filterOdd);
  };
  
  //controller
  someControllerAction = function() {
    User.getOdd().then(redirectToHomepageIfEmpty);
  };
Callbacks:

  //model
  User.get = function(callback) {
    getData(callback)
  }
  User.getOdd = function(callback) {
    User.get(function(data) {
      if (typeof callback == "function") {
        callback(filterOdd(data));
      }
    });
  };

  //controller
  someControllerAction = function() {
    User.getOdd(redirectToHomepageIfEmpty)
  };
Notice how there's a little bit of noise required in the callback version of User.getOdd - i.e. if (typeof callback == "function"). Ideally this kind of noise is something that should be hidden at a framework level, and not show up in application code.

Noise can get quite significant when the code is predominantly dealing with asynchrony, e.g. try implementing this in callback style:

  jQuery.when(User.getOdd(), Pinterest.search("something")).then(doSomething)
It's doable, but it's messy, because the code that is supposed to be focused on pinterest items and odd users now also needs to deal with flags indicating which http request is done at which point.


I'm not sure this makes a strong case. Especially since in this case the callback could be "simplified" by having it such that the callback example looks like:

    void loadData = function(){
      showLoadingScreen();
      $.ajax("http://someurl.com", {
        complete: hideLoadingScreen(function(data){
          //do something with the data
        })
      });
    };
Where hideLoadingScreen is simply:

    function hideLoadingScreen(f) {
        return function(e) {
            f(e);
            doHide();
        };
    }
You could probably take this further. Though, I'm not sure how valuable that is.


In your code, loadData() function knows that the data is being fetched asynchronously. In the posted code, loadData() doesn't have to know whether getData() works synchronously or asynchronously.


Meh. With higher order functions, this could be achieved as well. I was focusing on the "hide the loading screen" portion of the example.

And, honestly, it is not obvious to me when the loading screen gets hidden. In the callback one, that is much more obvious. (Granted, this is as much from familiarity.)


Hiding synchronous vs. asynchronous with higher-order functions means doing it with callbacks. It's generally hard to take a function that was originally written to run synchronously and change it to run asynchronously without changing the way it's called.

I don't see this as world-changing either, but I understand why people like it.


It's about reification...

Callbacks are an implementation of asynchronous processing each slightly different sort of like 'function calls / calling convention' in assembler, while promises provide a reified interface that is easier to think about conceptually because you're thinking about what to do, not how to do it.

The contrived example in SO can just as easily be decoupled while still using callbacks, however the promise based code 'looks' better because it more concisely describes what's going on in terms of 'english'.


Here's an example which might explain the decoupling a bit:

    // You may call this syntactic sugar
    var p1 = new Promise(function(response, reject) {
        setTimeout(function(){ 
            response(Math.random() * 1000); 
        });
    }).then(function(val){ console.log(val); return val; });
    
    // But here's the magical decoupled call
    p1.then(function(val) { console.log(val); });


I can't work out if this is an “Async Tarpit” or an instance of the “Blub Paradox”

Callbacks vs. Promises vs. Futures - they can all express the same things - personally I like promises, but YMMV




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: