Nonsense

Almost a year ago, I wrote up an "ELI5" about Promises. I always strive to explain things simply and clearly, but I feel I may have gone too far with the “LI5” aspect of it. So, this is a rewrite of that explanation, aiming to be clear and simple but also better structured.

Promises

Promises are one of the mechanisms for asynchronism available in JavaScript. While I have already explained the motivation behind them, this piece aims to provide a general overview of how they work or how one person could arrive at the concept.

I'm coming back to you

For a lengthy period of time, executing asynchronous tasks in JavaScript meant using continuations or callbacks. This means little more than “passing functions around”.

Let's imagine you and I are given a task for producing and presenting a certain value. We distribute our efforts and I pick the responsibility of producing the value, and you pick the responsibility for presenting it. Now, in very general terms, I might write a function such as…

function produceValue() {

    // Whatever operations are needed here.
    
    return value;
}
// To be called as:
// let result = produceValue();

…while you might write another function that uses de produced value, such as…

function renderPage(data) { ... }

let result = producedValue();
renderPage(result);

But as we're working on this, we realize that, for me to produce the value, I'll have to make some external calls (say, access a remote service or query a database). I could block execution while I wait to produce my value, but as we all know this is generally not a good idea. So, instead, we'll need to make my function work asynchronously.

So I change my interface and now my produceValue function expects that you pass a function and I will call that function when I finally do have the resulting value. Putting it into code, a language that I think we'll understand, we now have something like this:

function produceValue(callback) {

    // Whatever operations are needed here.
    
    // Instead of returning I do:
    callback(value);
}

// And on the other side, when you call my function, you'd do:
produceValue(renderPage);

This is simple, but it has a number of (somewhat) unexpected changes.

A pocketful of mumbles

The most salient change is that now I have to call your function. You call my function and pass yours so that I call it. Uff. Not only does it sound a bit too roundabout, but it reflects the more profound change of responsibility and control. Now, I have control over the correct continuation of your code. This is not only an unwanted coupling but also an unintended reversal of responsibilities.

And, so, trying to come up with a scheme that somehow allows us to avoid this reversal of control, we devise the following idea.

We plant a sort of proxy in between us. I will, in my function, create that proxy and immediately (i.e. synchronously) return that proxy to you when you call. Then, when I finally produce the value, instead of returning (which I can't) and instead of calling your function (which we don't want), what I'll do is sort of emit a signal or an event on that proxy object I gave you. That way you get notified of the availability of the value and you can decide what to do, without me controlling that execution.

Something like…

function produceValue() {

    let proxy = new Observable(); // Not really, but conceptually close to this

    // Enqueue my asynchronous operations here.
    // When my asynchronous operations finally produce the value then:
    // proxy.emit({ solved: true, result: value });
    
    // Return some proxy object synchronously:
    return proxy;    
}

This way, your code retains execution control. Or at least, instead of delegating that on my code, it does so on the proxy we've set as middleman.

let p = produceValue();

p.on('solved', renderPage); // Not really, but the idea is similar

Note that not only we solved that part of the problem. Freeing my code from the responsibility of executing your continuation, was done by introducing an abstract, agnostic entity between us. That entity (the Promise) knows nothing about either of us. It is independent of both our codes' concerns. This is important, because it will allow us to model a number of behaviours and capabilities into it in a generic and agnostic way. E.g. we may add a path for failure (i.e. communicating that the value could not be produced) (reject / catch)), or we may build a completely generic function that manages “waiting” for more than one delayed value (Promise.all). Or other ideas.

Note also that there still remain some other relevant problems in asynchronous code. Promises are not a be-all-end-all solution. They still have some complexities, such are promises. But so are all solutions, anyway.