15

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

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?

Kaiido
  • 123,334
  • 13
  • 219
  • 285
Stretch0
  • 8,362
  • 13
  • 71
  • 133
  • 1
    My understanding is that that's the whole point of a promise. – Feathercrown Oct 12 '16 at 00:53
  • *"does it act similar to a callback"* - In a general sense, a callback may be called asynchronously *or* synchronously depending on the context. But the whole point of promises is to allow async (non-blocking) operations. – nnnnnn Oct 12 '16 at 01:18
  • There is no "stack" in the sense that you appear to be using it in. I think you mean "event loop". Perhaps you should edit your title and also body of your question. –  Oct 12 '16 at 04:58

4 Answers4

17

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.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • 4
    I believe it depends on whether or not the logic executed in the promise is blocking. CPU intensive logic _will_ block the event loop regardless of any wrapping promises. – rob Jul 11 '19 at 19:28
  • "As such a promise doesn't block anything and certainly does not block the event loop.", then try running `const p = () => Promise.resolve().then(p); p()`. – Kaiido Oct 07 '20 at 00:41
  • @Kaiido - That looks to me like it would create an infinite loop? What possible problem are you solving with that? And, why would you do that? If you're just trying to counter my logic, please counter it with actual useful code rather than purposely harmful code. – jfriend00 Oct 07 '20 at 00:46
  • Yes it does create an inifnite loop that will definitely block the event loop. Your first paragraph is off. – Kaiido Oct 07 '20 at 00:47
  • @Kaiido - So, you're arguing that malicious code can be written using promises that will mess up the event loop. Yep, you can do that if you really want to. I'm not sure what value you just added to this discussion. And, my sentence stands as written. No single promise is blocking the event loop. Your infinite loop is blocking the event loop by repeatedly running the same function over and over again. – jfriend00 Oct 07 '20 at 00:50
  • Yes the Promise is blocking the event loop, it's not processed in parallel. If it wasn't blocking the event loop, like `const fn = () => setTimeout(fn); fn()` then your browser wouldn't freeze when running this line of code. So this single line of code proves that the event loop is blocked by Promises, contrarily to what you affirm. – Kaiido Oct 07 '20 at 00:52
  • @Kaiido - To, the event loop is not actually blocked in your example. Non-promise events are starved (they never get a chance to run) because you're in an infinite loop constantly putting another resolved promise in the event queue over and over and over so all the event loop can ever do is serve each new promise and it never gets a chance to serve any lower priority events. This isn't a blocked event loop as the event loop is still regularly doing its thing. Instead, you've flooded it with a constant stream of high priority promises so the lower priority events never get a chance to run. – jfriend00 Oct 07 '20 at 00:57
  • @Kaiido - Either way, you've ruined the ability to process regular events by flooding it with high priority promises. The problem is repeatedly flooding it with high priority events. What are you expecting me to change in this answer. Do you want me to add a disclaimer that the event loop can get starved of its ability to process regular events if you create an infinite loop constantly adding a new resolved promise to the event loop over and over and over again. – jfriend00 Oct 07 '20 at 00:57
  • No, Promises have no priority, they are executed synchronously after the task where they got resolved. – Kaiido Oct 07 '20 at 01:00
  • @Kaiido - Promises are put ahead of some other things in the event loop. There is a priority system. Yes, the `.then()` handler is executed after the task that resolved it finishes, but if there are other things in the event loop already waiting, the resolved promise will jump in line ahead of those other things. That's what can starve the event loop from serving other things if you are continually stuffing a resolved promise into the event loop. Those other things that are already waiting to run never get a chance. – jfriend00 Oct 07 '20 at 01:01
  • yes there is a priority system but Promises are not part of it at all. Promises are executed synchronously as part of the various [microtask-checkpoints](https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint) inside the event loop. The task prioritization system only relates to the first step of the [event loop](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model) where the browser has to choose which task to execute. Promises are microtasks and thus not chosen here. – Kaiido Oct 07 '20 at 01:06
  • If it starves the browser it's because by spawning a new microtask in the microtask-checkpoint we never leave this microtask-checkpoint and we indeed completely block the event loop. – Kaiido Oct 07 '20 at 01:08
  • @Kaiido - I disagree with some of your understanding and some of what you are arguing is merely terminology, but I don't feel like debating it with you here in the comments here as it's fairly off topic to this particular question. What could be on topic is what do you think needs to be added to this answer to cure your objection? I have no objection to improving this answer if there's a way we can agree on to do that. – jfriend00 Oct 07 '20 at 01:18
  • Unfortunately I don't see any other mean than posting an other answer entirely... – Kaiido Oct 07 '20 at 01:21
  • @Kaiido - You have one sentence you don't like and you can't suggest any changes to it that you like? – jfriend00 Oct 07 '20 at 01:22
  • @Kaiido - So, you don't think the processing of resolved promises has anything to do with the event loop? You might want to read this [What is the relationship between event loop and promise](https://stackoverflow.com/questions/46375711/what-is-the-relationship-between-event-loop-and-promise). To me, the continued processing of resolved promises is still the event loop doing something. It's not blocked - it's processing microtasks. In your infinite loop where you are continually causing a new microtask to get added to the queue, nothing other than microtasks will ever get a chance to run. – jfriend00 Oct 07 '20 at 01:29
  • @Kaiido - But, yes macrotasks don't get a chance to run if you continue to overwhelm the queue with constant microtasks. This the prioritization I was referring to. This is something a Javascript programmer has to avoid doing. Fortunately, it doesn't come up very often in code that isn't trying to be malicious. I have only seen it occur by mistake once in my own code when I was juggling a whole bunch of promise-based things and trying to keep 8 Worker Threads as busy as possible. – jfriend00 Oct 07 '20 at 01:31
  • This is not prioritization, browsers don't have the choice to set anything else in between, there is no priority involved at all. Prioritization allows the browsers tell that the message queued on the network-tasksource is less important than the one queued on the UI-tasksource and thus they'll choose to execute the mouseevent's callback before the fetch's one. But they can also choose to finally execute that fetch callback if they think it has been waiting for too long, even if there are other higher priority tasks that came in between. See https://github.com/WICG/main-thread-scheduling/ – Kaiido Oct 07 '20 at 01:54
  • @Kaiido - You're just arguing terminology as has pretty much always been the case here. There seems to be no dispute about what actually happens here inside the event loop. I'm open to a suggestion for how to change the one sentence you object to. If no suggestion forthcoming, then no need to further discuss. – jfriend00 Oct 07 '20 at 02:04
  • Yes there is a very strong "dispute" here, your first paragraph is plain wrong. The event loop is blocked by Promises. Now, that's your answer, I only pointed to the flaw in it. Your call to find the proper way to improve it now that you know what you said in there is not correct. My take was to write a [new answer](https://stackoverflow.com/a/64236082/3702797) here so people don't get mislead by yours. – Kaiido Oct 07 '20 at 02:14
10

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
Kaiido
  • 123,334
  • 13
  • 219
  • 285
6

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.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 4
    "An await blocks only the current async function" --- I think it needs better wording, "blocks" here it confusing. – zerkms Oct 12 '16 at 01:04
  • @zerkms Any suggestions? Actually I think "[blocking](https://en.wikipedia.org/wiki/Blocking_(computing))" is quite fitting if you consider every call to an `async function` to create a task/process. – Bergi Oct 12 '16 at 10:32
  • Perhaps. The whole `async/await` concept is not the easiest thing to explain and doing it with right words is over my language abilities. – zerkms Oct 12 '16 at 20:06
2

As other mentioned above... Promises are just like an event notification system and async/await is the same as then(). However, be very careful, You can "block" the event loop by executing a blocking operation. Take a look to the following code:

function blocking_operation_inside_promise(){
    return new Promise ( (res, rej) => {
        while( true ) console.log(' loop inside promise ')
        res();
    })
}

async function init(){
    let await_forever = await blocking_operation_inside_promise()
}

init()
console.log('END')

The END log will never be printed. JS is single threaded and that thread is busy right now. You could say that whole thing is "blocked" by the blocking operation. In this particular case the event loop is not blocked per se, but it wont deliver events to your application because the main thread is busy.

JS/Node can be a very useful programming language, very efficient when using non-blocking operations (like network operations). But do not use it to execute very intense CPU algorithms. If you are at the browser consider to use Web Workers, if you are at the server side use Worker Threads, Child Processes or a Microservice Architecture.

David Valdivieso
  • 449
  • 1
  • 5
  • 11