Please be patient with the following explanation. It will, inevitably, be long and, while I'll try to avoid it, at times it may even get a bit tedious. There will be a summary at the end, but please read it all before going there.

Say we, you and me, have to build a part of a system. You're going to be building some utilities that I will be using somewhere else.

Let's start with something very basic

So, for example, we have a need where you'll have to build some code that returns 4 that I will then use. (I know it sounds silly, but let's not complicate things right from the start).

So you do this piece of code…

    // do whatever calculation to get that 4 and...
    let a = 4;

…and say: “Hey, just use a”. And, of course, I'll say: “No way. Give me a function that I can call and will give me back 4”. So, oh, ok, you do…

    function four() {
        // do whatever calculation you need to get that 4 and...
        return 4;
    }

And now all is cool. Of course this is too silly as it is, but we have justified the first need for functions: when you need to reuse some code, you put it inside a function.

Ok, now, the part of the system we will be doing is a simple test system. You will be building functions that test some processes in our system. e.g. you may have a function such as…

    function testThatFourIsFour() {
        let obtainedFour = four();
        console.log("Is four four? " + (obtainedFour === 4) ? true : false);
    }

But not exactly like that because you want to focus on the test itself but not necessarily on things like putting the result in the console. We may want our output to go somewhere else. We may want our tests to stop executing if one fails. Or other concerns that are not the responsibility of the tests themselves but of the test system. And that's the part that I will be doing.

So I will make a function test, and you will be passing your test to that function. Like this…

    test(function testThatFourIsFour() {
        let obtainedFour = four();
        console.log("Is four four? " + (obtainedFour === 4) ? true : false);
    });

But how does this help?? You still have to worry about console.log and all that! No, because instead of using console.log I will give you a function you can use in its place. This function will be called assertTrue and it will be that function which manages if the result is right or wrong and output the corresponding message where we want it to be outputted.

And how do I “give you this assertTrue function”? Well, I could just write that function as a global, but that's just ugly. (Not only, there are other reasons, but for now let's not get too concerned about that.) So instead all the functions/tests you write will receive an argument, and my function test will pass that assertTrue function to your tests as that argument. So…

    test(function testThatFourIsFour(assertTrue) {
        let obtainedFour = four();
        assertTrue(obtainedFour === 4, "Four is four.");
    });

Does this seem alright with you? I hope it does, because with this, we have a valid justification to “pass a function that in turn receives another function”.

Asynchronous life

Ok, so now let's keep those ideas there and we will start to talk about asynchronous things.

Now, you will be writing a function that returns some contents that you will read from a file or a database or by calling another server or something of that kind. And I will be using your function.

Naïvely, I could want, given your function readFourFromDB, to just do this:

    // just doing my stuff when suddenly I need a four!
 
    let obtainedFour = readFourFromDB();
 
    // Now that I have a four I do some more stuff...
    doSomeStuff(obtainedFour);

But, of course, there is a problem. The problem is that your function readFourFromDB takes ~2 whole seconds! OMG! And it cannot be avoided because the DB is in another server at the other side of the globe, and the network is slow and we're living in the eighties! Oh, noes, where's my pink Raybans!

Ok, so you know how to use callbacks and you you tell me you've re-written readFourFromDB so that I can give you a callback function and then you will call that function with the result when you have it. My code, would look something like this…

    readFourFromDB(function(obtainedFour) {
        // Now that I have a four I do some more stuff...
        doSomeStuff(obtainedFour);
    });

But I look back at my code and I also have a need for a Five and a Six! And know my code looks sort of like this:

    readFourFromDB(function(obtainedFour) {
        // Now that I have a Four I also need a Five...
        readFiveFromDB(function(obtainedFive) {
            // And now that I have the Four and the Five I also need a Six...
            readSixFromDB(function(obtainedSix) {
                // Now I have everything...
                doSomeStuff(obtainedFour, obtainedFive, obtainedSix);
            });
        });
    });

Wow, this is getting ugly pretty quick. Just think that now everything I want to do “after” calling your function has to go inside a callback. So we call for a meeting and we look for a better solution (for some particular meaning of “better”). And the better solution is this: Whenever I call you, you will immediately give me something back.

    let something = readFourFromDb();

“But” - you say - “I cannot give you what I don't yet have!” And you're right. But I didn't say you have to give me the real result immediately, just “something” I can work with. So what can you give me that is not the result but makes some sense and I can work with?

You give me back a sort of proxy for the result. A thing which is not the result, but that I know at some point will contain the result. And we devise this strange arrangement…

    // You do this...
    function readFourFromDB() {
        var result = { value: null };
        setTimeout(function() {
            result.value = 4;
        }, 2000);
        return result;
    }
 
    // I do this...
    let ThisWillBeSomethingLikeAFour = readFourFromDB();
    // I go on my marry way getting a Five and a Six...
    let ThisWillBeSomethingLikeAFive = readFiveFromDB();
    let ThisWillBeSomethingLikeASix = readSixFromDB();

Now, the only problem for me, for my code, is that I don't know when ThisWillBeSomethingLikeAFour.value actually gets filled with the result I need. But at least, we have solved the part were my code was all in callbacks, deeper and deeper each time.

I could just be lazy and do some…

    setTimeout(function() {
        console.log(ThisWillBeSomethingLikeAFour.value + ":" + ThisWillBeSomethingLikeAFive.value + ":" + ThisWillBeSomethingLikeASix.value);
    }, 10000);

10 seconds will surely be enough, right? And it does work (just wait for ten seconds xD). It works but, of course, you can see this is ridiculous. I can go on my way with my code but having to wait “enough time” just in case is not a solution. I could, surely, do better. I could just wait a bit, see if ThisWillBeSomethingLikeAFour.value !== null, if not, wait a bit more, see again… Which, yes, is better, but… not really better. Just less bad.

So we call another meeting and think about this some more. And the obvious solution is going back to callbacks. But I don't want to do that. And besides what good would it be that you give me those proxy thingies back, if I still have to give you a callback that you will call? So we look up some old CS books and we find an abstraction for passing callbacks around.

Instead of sharing an object like { value: <null/eventuallyResultValue> } we will share an object like { valueIsReady: function() {} }. That is, we will share an object not with a static value, but with a function. You will call that function and so I will get notified that the value is ready. So, you now have something like this…

    function readFourFromDB() {
        var result = { valueIsReady: function(value) { ... } };
        setTimeout(function() {
            result.valueIsReady(4);
        }, 2000);
        return result;
    }

Weeell… sort of… Uhmm… There's a problem for me now. Because you call that function, but how exactly do I know you've called it? Bear with me, this one's going to be weird: I add something to the result object and valueIsReady will call that!

    // You do...
    function readFourFromDB() {
        var result = { valueIsReady: function(value) { this.done(value); } };
        setTimeout(function() {
            result.valueIsReady(4);
        }, 2000);
        return result;
    }
 
    // And I do...
    var willBeAFourOrSomething = readFourFromDB();
    willBeAFourOrSomething.done = function(value) {
        console.log(value);
    };

Yes, yes, I know, this seems to be going around in circles. (Have patience please) What have we gained from all this? Again this works, but it looks seriously similar to having a callback anyway! You're right, it looks similar, but we have gained one thing: Now, I'm not passing the callback to you. We're sharing that object and I'm passing the callback to that object, not to you.

That is progress. Not much, but it is. And it's progress because we've somewhat decoupled our codes a bit. I call your function, you give me back “something” and from there on I only work with that “something” and you only operate on that “something”. I'm not passing my code to you, and you're not responsible of calling my code.

Next step, clearly, is to standardize things a bit with that result object thing, because it's something we will be using a lot and it pays to clean up. So instead of always having to write var result = { valueIsReady: function(value) { this.done(value); } }; in all your functions, you will do…

    var result = new SharedThingie();

…because we will have some constructor like…

    function SharedThingie() {
        return {
            valueIsReady: function(value) { this.whenValueIsReadyDo(value); },
            whenValueIsReadyDo: function() { }
        };
    }

We set a dummy whenValueIsReadyDo so no errors are thrown in case I forget to assign one.

All this is just a simple cleanup. But there are two bigger things we could fix… Firstly it's kind of ugly that in order to set the “callback” I have to do…

    thing.whenValueIsReadyDo = function(value) { ... };

…because, you know, we're not animals, overwriting methods! We can pass functions around! So we will change SharedThingie to something that can be used like…

    thing.whenValueIsReadyDo(function(value) { ... });

I'm intentionally not writing what the modified SharedThingie looks like, but you can imagine that it stores the function I give it somewhere so that it can call it later, ok?

The second thing we can fix is in your code. Let's look at it again in its current state:

    // You do...
    function readFourFromDB() {
        var result = new SharedThingie();
        setTimeout(function() {
            result.valueIsReady(4);
        }, 2000);
        return result;
    }

All your functions will follow this pattern. You set up the shared thingie, you have some asynchronous code, you return the thingie. This is boring. And ugly. There's a particularly ugly thing and that's that in your asynchronous code you will always have those result.valueIsReady(…). And it's ugly because not only we're repeating the name result (which is a name with very little meaning) everywhere but also because using .valueIsReady really exposes the shape of SharedThingie.

Now, a bit of an intermission about shapes and exposing them. If you don't feel like it, you don't need to read this paragraph; just assume that exposing the shape of things unnecessarily is bad form. And it is, because once we expose the shape (the interface) of an object, we're signing a contract with the code that uses it. We're saying “our object has this method called whatever”. And in the future, when I want to change my object and not use method whatever, I will have to break the contract and tell you “if you use the new version your code will break”. And so, we try to expose only as little as needed of the shapes of our objects and APIs. End of the intermission.

So, how do we, in just one step, both avoid exposing the shape of SharedThingie and let you clean up all those result here and result there? Well, instead of writing your code that way, you will pass your code into SharedThingie and it will execute it. So, something like…

    function readFourFromDB() {
        var result = new SharedThingie(
            setTimeout(function() {
                result.valueIsReady(4);
            }, 2000);
        );
        return result;
    }

Which of course doesn't work. If you remember how we started, when we want to pass around pieces of code, we put it into a function. So…

    function readFourFromDB() {
        var result = new SharedThingie(function() {
            setTimeout(function() {
                result.valueIsReady(4);
            }, 2000);
        });
        return result;
    }

But… but… Wait, wait, I know nothing's changed! We're not done yet! Now, instead of you calling result.valueIsReady, what we'll do is that SharedThingie will give you back something you can call… as an argument to that function where we've put our code. You remember, right? we also talked about this at the start. So with that and some clean up…

    function readFourFromDB() {
        return new SharedThingie(function(callThisWhenDone) {
            setTimeout(function() {
                callThisWhenDone(4);
            }, 2000);
        });
    }

So, now, your code has some boilerplate… but much less than it had with the previous version of SharedThingie! And it still maintains all the other advantages we had gained. So, your code is acceptably clean, my code is acceptable uncouple from yours… And we really don't care much what SharedThingie does on the inside as long as it works as we devised it.

You will have, naturally, understood a while ago that SharedThingie is in fact a sort of primitive Promise. As it is it only allows successful results from your code. This is not terribly important. We could do without it if we had to. I mean you could just return a null or a -1 or whatever. But, on the other hand, is not much of a problem to add the capability of signalling an error. We just add this: “Not only will SharedThingie pass you a callThisWhenDone it will also pass a second function callThisIfSomethingGoesWrong”. Your code would look like…

    function readFourFromDB() {
        return new SharedThingie(function(callThisWhenDone, callThisIfSomethingGoesWrong) {
            setTimeout(function() {
                if ((new Date()).getDay() !== 2) callThisWhenDone(4);
                else callThisIfSomethingGoesWrong("Oh, no, sir! Not on Wednesday!"); 
            }, 2000);
        });
    }

…and mine like…

    thing.whenValueIsReadyDo(function(value) { ... });
    thing.whenThingsGoBadDo(function(whaHappend) { ... });

Inside SharedThingie a lot can be improved. The original thing I showed is not only extremely basic but won't also has some failings (what if, for example, I set up the callback for whenValueIsReadyDo after you've already called callThisWhenDone?). But really, if you actually want to see the inner workings of a Promise implementation, there are a bunch of them around. And while it is interesting, you don't need to see it in order to understand what it does.

We can also improve our naming… Promise, when, resolve, reject, etc. Also, we can allow chaining and a bunch of other capabilities. But that's just adding stuff :)

Summary

So, what's the summary? Why do all this.

Well, while it may be discussed if Promises are a good device (or better than others) this is what it provides: It separates the client code from the asynchronous functions it uses. In particular, it provides an intermediate shared device both parts use to communicate so that on the one hand the client code retains control over its code (instead of passing a callback that the other side will be responsible for calling) and on the other, the asynchronous function can notify the other of the result of its operation in an “action agnostic” way (that is, it's just a notification, not a call to a particular client's callback).

The main benefit is it allows building some additional control structures for asynchronism in a generic, abstracting out the particular callbacks of each case.