Why does setting CSS property using Promise.then not actually happen at the then block?

The event loop batches style changes. If you change the style of an element on one line, the browser doesn't show that change immediately; it'll wait until the next animation frame. This is why, for example

elm.style.width = '10px';
elm.style.width = '100px';

doesn't result in flickering; the browser only cares about the style values set after all Javascript has completed.

Rendering occurs after all Javascript has completed, including microtasks. The .then of a Promise occurs in a microtask (which will effectively run as soon as all other Javascript has finished, but before anything else - such as rendering - has had a chance to run).

What you're doing is you're setting the transition property to '' in the microtask, before the browser has started rendering the change caused by style.transform = ''.

If you reset the transition to the empty string after a requestAnimationFrame (which will run just before the next repaint), and then after a setTimeout (which will run just after the next repaint), it'll work as expected:

const box = document.querySelector('.box')
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)'
    setTimeout(() => {
      box.style.transition = 'none'
      box.style.transform = ''
      // resolve('Transition complete')
      requestAnimationFrame(() => {
        setTimeout(() => {
          box.style.transition = ''
        });
      });
    }, 2000)
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class="box"></div>


You are facing a variation of the transition doesn't work if element start hidden problem, but directly on the transition property.

You can refer to this answer to understand how the CSSOM and the DOM are linked for the "redraw" process.
Basically, browsers will generally wait until the next painting frame to recalculate all the new box positions and thus to apply CSS rules to the CSSOM.

So in your Promise handler, when you reset the transition to "", the transform: "" has still not been calculated yet. When it will get calculated, the transition will already have been reset to "" and the CSSOM will trigger the transition for the transform update.

However, we can force the browser to trigger a "reflow" and thus we can make it recalculate the position of your element, before we reset the transition to "".

const box = document.querySelector('.box')
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)'
    new Promise(resolve => {
      setTimeout(() => {
        box.style.transition = 'none'
        box.style.transform = ''
        box.offsetWidth; // this triggers a reflow
        resolve('Transition complete')
      }, 2000)
    }).then(() => {
      box.style.transition = ''
    })
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class = "box"></div>

Which makes the use of the Promise quite unnecessary:

const box = document.querySelector('.box')
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)'
    setTimeout(() => {
      box.style.transition = 'none'
      box.style.transform = ''
      box.offsetWidth; // this triggers a reflow
      // even synchronously
      box.style.transition = ''
    }, 2000)
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class = "box"></div>


And for an explanation on micro-tasks, like Promise.resolve() or MutationEvents, or queueMicrotask(), you need to understand they'll get ran as soon as the current task is done, 7th step of the Event-loop processing model, before the rendering steps.
So in your case, it's very like if it were ran synchronously.

By the way, beware micro-tasks can be as blocking as a while loop:

// this will freeze your page just like a while(1) loop
const makeProm = ()=> Promise.resolve().then( makeProm );