Three ways of understanding Promises

[2016-10-27] dev, javascript, esnext
(Ad, please don’t block)

This blog post covers three ways of understanding Promises.

This is an example of invoking a Promise-based function asyncFunc():

function asyncFunc() {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve('DONE'), 100);
    });
}
asyncFunc()
.then(x => console.log('Result: '+x));

// Output:
// Result: DONE

So what is a Promise?

  • Conceptually, invoking asyncFunc() is a blocking function call.
  • A Promise is both a container for a value and an event emitter.

Conceptually: calling a Promise-based function is blocking  

function asyncFunc() {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve('DONE'), 100);
    });
}
async function main() {
    const x = await asyncFunc(); // (A)
    console.log('Result: '+x);

    // Same as:
    // asyncFunc()
    // .then(x => console.log('Result: '+x));
}
main();

main() is an async function. Its body expresses well what’s going on conceptually – how we usually think about asynchronous computations:

  • Line (A): Wait until asyncFunc() is finished.
  • Line (B): Then log its result x.

Prior to ECMAScript 6 and generators, you couldn’t suspend and resume code, which is why, for Promises, you put everything that happens after the code is resumed into a callback. Invoking that callback is the same as resuming the code.

A Promise is a container for an asynchronously delivered value  

If a function returns a Promise then that Promise is like a blank into which the function will (usually) eventually fill in its result, once it has computed it. You can simulate a simple version of this process via an Array:

function asyncFunc() {
    const blank = [];
    setTimeout(() => blank.push('DONE'), 100);
    return blank;
}
const blank = asyncFunc();
// Wait until the value has been filled in
setTimeout(() => {
    const x = blank[0]; // (A)
    console.log('Result: '+x);
}, 200);

With Promises, you don’t access the eventual value via [0] (as in line (A)), you use method then() and a callback.

A Promise is an event emitter  

Another way to view a Promise is as an object that emits events.

function asyncFunc() {
    const eventEmitter = { success: [] };
    setTimeout(() => { // (A)
        for (const handler of eventEmitter.success) {
            handler('DONE');
        }
    }, 100);
    return eventEmitter;
}
asyncFunc()
.success.push(x => console.log('Result: '+x)); // (B)

Registering the event listener (line (B)) can be done after calling asyncFunc(), because the callback handed to setTimeout() (line (A)) is executed asynchronously, after this piece of code is finished.

Normal event emitters specialize in delivering multiple events, starting as soon as you register.

In contrast, Promises specialize in delivering exactly one value and come with built-in protection against registering too late: the result of a Promise is cached and passed to event listeners that are registered after the Promise was settled.

Further reading