The following is distilled from a discussion on our #javascript Slack channel.

Consider the following:

let counter = 0;
const increment = new Promise(resolve => {
  counter++;
  resolve();
});
await increment;
await increment;
console.log(counter); // 1? 2? something else?

What value do we expect counter to have?

What is a promise? Does it mean “do this later”?

It’s more accurate to think of promises as state machines.
A promise begins its life in a “pending” state. If you ask for the result whilst it’s in this state: you’ll have to queue for it.

The Promise constructor above calls our executor function immediately. Its counter++ side-effect runs. resolve() is called with no arguments, storing an undefined result on the promise and promoting its state to “fulfilled”.

The first await runs. await is equivalent to running .then(onFulfilled) upon your promise, with onFulfilled set to “the code ahead”. This work is enqueued as a microtask. JavaScript executes microtasks in FIFO order; control returns to our function eventually.

The second await is no different. It creates a microtask to “give me the promise result and run the code ahead”, and waits to be scheduled by JavaScript.

Our side-effect only ever ran once — during Promise construction.
counter is 1.

Did we defer any work? No; the promise was created and fulfilled synchronously. But we used await to yield control to other microtasks.

How would we defer work?

const microtask = Promise.resolve()
.then(() => console.log('hello from the microtask queue'));

const macrotask = new Promise(resolve =>
  // enqueue a macrotask
  setTimeout(() => {
    console.log('hello from the macrotask queue');
    resolve();
  })
  // we did not resolve; promise remains in pending state
);

// this is logged first; we succeeded in deferring work
console.log('hello from the original execution context');

// yields to scheduler; .then() reaction runs (and logs)
await microtask;
// yields to scheduler; promise already fulfilled, so nothing logged
await microtask;

// yields to scheduler; all microtasks run, then macrotask runs (and logs)
await macrotask;
// yields to scheduler; promise already fulfilled, so nothing logged
await macrotask;

The Promise constructor runs synchronously, but we do not have to call resolve() synchronously. Promise.prototype.then also defers work.

Neither the Promise constructor nor Promise.prototype.then repeat work.
The implication of this is that promises can be used to memoize async computations. If you consume a promise whose result will be needed again later: consider holding on to the promise instead of its result!

It’s fine to await a promise twice, if you’re happy to yield twice.