Do Javascript promises block the stack

When using Javascript promises, does the event loop get blocked?

No. Promises are only an event notification system. They aren't an operation themselves. They simply respond to being resolved or rejected by calling the appropriate .then() or .catch() handlers and if chained to other promises, they can delay calling those handlers until the promises they are chained to also resolve/reject. As such a single promise doesn't block anything and certainly does not block the event loop.

My understanding is that using await & async, makes the stack stop until the operation has completed. Does it do this by blocking the stack or does it act similar to a callback and pass of the process to an API of sorts?

await is simply syntactic sugar that replaces a .then() handler with a bit simpler syntax. But, under the covers the operation is the same. The code that comes after the await is basically put inside an invisible .then() handler and there is no blocking of the event loop, just like there is no blocking with a .then() handler.


Note to address one of the comments below:

Now, if you were to construct code that overwhelms the event loop with continually resolving promises (in some sort of infinite loop as proposed in some comments here), then the event loop will just over and over process those continually resolved promises from the microtask queue and will never get a chance to process macrotasks waiting in the event loop (other types of events). The event loop is still running and is still processing microtasks, but if you are stuffing new microtasks (resolved promises) into it continually, then it may never get to the macrotasks. There seems to be some debate about whether one would call this "blocking the event loop" or not. That's just a terminology question - what's more important is what is actually happening. In this example of an infinite loop continually resolving a new promise over and over, the event loop will continue processing those resolved promises and the other events in the event queue will not get processed because they never get to the front of the line to get their turn. This is more often referred to as "starvation" than it is "blocking", but the point is that macrotasks may not get serviced if you are continually and infinitely putting new microtasks in the queue.

This notion of an infinite loop continually resolving a new promise should be avoided in Javascript. It can starve other events from getting a chance to be serviced.


Do Javascript promises block the stack

No, not the stack. The current job will run until completion before the Promise's callback starts executing.

When using Javascript promises, does the event loop get blocked?

Yes it does.

Different environments have different event-loop processing models, so I'll be talking about the one in browsers, but even though nodejs's model is a bit simpler, they actually expose the same behavior.

In a browser, Promises' callbacks (PromiseReactionJob in ES terms), are actually executed in what is called a microtask.
A microtask is a special task that gets queued in the special microtask-queue. This microtask-queue is visited various times during a single event-loop iteration in what is called a microtask-checkpoint, and every time the JS call stack is empty, for instance after the main task is done, after rendering events like resize are executed, after every animation-frame callback, etc.

These microtask-checkpoints are part of the event-loop, and will block it the time they run just like any other task.
What is more about these however is that a microtask scheduled from a microtask-checkpoint will get executed by that same microtask-checkpoint.

This means that the simple fact of using a Promise doesn't make your code let the event-loop breath, like a setTimeout() scheduled task could do, and even though the js stack has been emptied and the previous task has been executed entirely before the callback is called, you can still very well lock completely the event-loop, never allowing it to process any other task or even update the rendering:

const log = document.getElementById( "log" );
let now = performance.now();
let i = 0;

const promLoop = () => {
  // only the final result will get painted
  // because the event-loop can never reach the "update the rendering steps"
  log.textContent = i++;
  if( performance.now() - now < 5000 ) {
    // this doesn't let the event-loop loop
    return Promise.resolve().then( promLoop );
  }
  else { i = 0; }
};

const taskLoop = () => {
  log.textContent = i++;
  if( performance.now() - now < 5000 ) {
    // this does let the event-loop loop
    postTask( taskLoop );
  }
  else { i = 0; }
};

document.getElementById( "prom-btn" ).onclick = start( promLoop );
document.getElementById( "task-btn" ).onclick = start( taskLoop );

function start( fn ) {
  return (evt) => {
    i = 0;
    now = performance.now();
    fn();
  };
}

// Posts a "macro-task".
// We could use setTimeout, but this method gets throttled
// to 4ms after 5 recursive calls.
// So instead we use either the incoming postTask API
// or the MesageChannel API which are not affected
// by this limitation
function postTask( task ) {
  // Available in Chrome 86+ under the 'Experimental Web Platforms' flag
  if( window.scheduler ) {
    return scheduler.postTask( task, { priority: "user-blocking" } );
  }
  else {
    const channel = postTask.channel ||= new MessageChannel();
    channel.port1
      .addEventListener( "message", () => task(), { once: true } );
    channel.port2.postMessage( "" );
    channel.port1.start();
  }
}
<button id="prom-btn">use promises</button>
<button id="task-btn">use postTask</button>
<pre id="log"></pre>

So beware, using a Promise doesn't help at all with letting the event-loop actually loop.

Too often we see code using a batching pattern to not block the UI that fails completely its goal because it is assuming Promises will let the event-loop loop. For this, keep using setTimeout() as a mean to schedule a task, or use the postTask API if you are in a near future.

My understanding is that using await & async, makes the stack stop until the operation has completed.

Kind of... when awaiting a value it will add the remaining of the function execution to the callbacks attached to the awaited Promise (which can be a new Promise resolving the non-Promise value).
So the stack is indeed cleared at this time, but the event loop is not blocked at all here, on the contrary it's been freed to execute anything else until the Promise resolves.

This means that you can very well await for a never resolving promise and still let your browser live correctly.

async function fn() {

  console.log( "will wait a bit" );
  const prom = await new Promise( (res, rej) => {} );
  console.log( "done waiting" );
  
}
fn();

onmousemove = () => console.log( "still alive" );
move your mouse to check if the page is locked

An await blocks only the current async function, the event loop continues to run normally. When the promise settles, the execution of the function body is resumed where it stopped.

Every async/await can be transformed in an equivalent .then(…)-callback program, and works just like that from the concurrency perspective. So while a promise is being awaited, other events may fire and arbitrary other code may run.