-1
async function() {
  while (true) {
    await null;
  }
}

This will block the thread forever, because the event loop would try to finish all microtasks in one frame.

(async () => {
  for (let i = 0; i < 999999; i++) {
    await null;
    document.body.appendChild(document.createElement("span"));
    console.log("running")
  }
  console.log("finished")
})();

With timeout be like:

  1. Event loop suspends processing next microtask after 1 sec.
  2. The engine use the thread to process other code, for example, animation frame.
  3. Then in the next frame, event loop continues to process microtasks from last frame.

So the while (true) will block the thread only for 1 sec per frame, because await null; is suspendable.

Polyfill: yield as await

const tasks = new Array;
const resolvedValues = new Array;
const runnables = new WeakSet;

function process(task, resolved) {
  try {
    const result = task.next(resolved);
    if (result.done) {
      return null;
    }
    const { value } = result;
    if (value === undefined) {
      tasks.unshift(task);
      resolvedValues.unshift(undefined);
      return;
    }
    Promise.resolve(value).then((value) => {
      tasks.unshift(task);
      resolvedValues.unshift(value);
      if (unsubscribed) {
        unsubscribed = false;
        requestAnimationFrame(listener)
      }
    })
  } catch (e) {
    console.error(e)
  }
  return null;
}

let unsubscribed = true;
const listener = function() {
  const ms = Date.now();
  while (Date.now() - ms < 100) {
    if (process(tasks.pop(), resolvedValues.pop()) === null) {
      break;
    }
  }
  if (tasks.length > 0) {
    requestAnimationFrame(listener);
    return;
  }
  unsubscribed = true
};

function suspend(runnable) {
  const task = runnable();
  if (runnables.has(task)) {
    return;
  }
  runnables.add(task);
  if (process(task) === null) {
    return;
  }
  if (unsubscribed) {
    unsubscribed = false;
    requestAnimationFrame(listener)
  }
}
export { suspend };
let n = 0;
suspend(function*() {
  while (true) {
    yield;
    n++;
    console.log("running")
  }
});

const resetCount = () => {
  console.log(n);
  n = 0;
  requestAnimationFrame(resetCount);
};
requestAnimationFrame(resetCount);
suspend(function*() {
  for (let i = 0; i < 999999; i++) {
    yield;
    document.body.appendChild(document.createElement("span"));
    console.log("running")
  }
  console.log("finished")
});

"that might break things", I agree, but what might be broken exactly?

I don't mean I must have this feature. I'd like to know why event loop was not designed like that, before microtasks/promises started existing, at that moment no backwards compatibility was needed.

  • It would be kind of annoying when your code would run millisecond to long and get timeout out randomly because if this. Also, there could be a lot of things in JavaScript, but there aren't because of backwards compatibility and stuff. – Konrad Jul 08 '22 at 19:48
  • 3
    Why should it? That might break things. If you want a timeout, or to hand off the baton to other areas of code, you should explicitly tell the engine you want to do that. – Samathingamajig Jul 08 '22 at 19:49
  • 5
    I don't see the point of the question.. Even without async/await that `while(true)` will block the thread. – Cristian-Florin Calina Jul 08 '22 at 20:28
  • 1
    I don't understand the question. Where do you expect a timeout? There is no timeout in the code. Can you elaborate, please? – Bergi Jul 08 '22 at 20:37
  • 1
    "can be used to process other code, for example, animation frame" which themselves will run a new microtask checkpoint (actually several). This would mean having multiple microtask queues, based on not very clear rules, because if we take for instance one per step of the event loop, you could very well have `requestAnimationFrame(() => (function loop () { Promise.resolve().then(loop); })());` and you'd block the pseudo "animation frame's microtask queue", and back to square one. – Kaiido Jul 11 '22 at 01:59

2 Answers2

2

[W]hat might be broken exactly?

Any code that assumes that all the microtasks are processed before the next task for one.
Obviously native Web APIs that do use microtasks are designed in a way they don't enter such a loop, but user-land code may need something close to it (for instance awaiting every Promises in a very long Array.
So yes, they could probably detect that they entered an infinite loop, probably more easily with your async function than with a (function loop() { return Promise.resolve().then(loop); })() and put an halt to it like they do actually detect while(true){} loops. But for the synchronous version, they don't actually analyze the code, they do measure how long each task (or microtask) lasts, and since in our microtask loops each microtask takes no time, they get out of the radar.
So I wouldn't be surprised that in the future we'll have the same "A script is blocking your tab" kind of message even for microtask loops, but that's probably not something vendors want to spend too much time on at the moment since this is really just being nice for bad code.

But if such a fools-guard exists, it should not resume the microtask, but simply halt and destroy it.

[W]hy [the] event loop was not designed like that

Because by the original design microtasks are a tool to coalesce small events that occurred in a task, so that all these events are treated in one go after the full task is done, but before the next task kicks in. For instance, one of the original APIs that lead to the design of the microtask queue as it is today is the MutationRecords API, where we want to handle all the DOM changes before the next painting frame. Allowing to "pause" the microtask checkpoint in the middle would break the promise that we handled all our microtasks before the next task starts, and still using the MutationRecords example, we could see the DOM changes get rendered before we invalidate them at the next task.

Also, keep in mind that the microtask queue is visited many times per event loop iteration. Basically every time a script is executed, it will trigger a microtask checkpoint. So for your design to work we'd need several microtask queues, maybe one per step of the event loop processing model. But then, we could have one requestAnimationFrame callback blocking the event loop, and what happens to the following callback?

Kaiido
  • 123,334
  • 13
  • 219
  • 285
-2

Could it be possible for a JavaScript engine to detect potentially bad code?

Yes!

Is this useful?

Certainly could be. After all, TypeScript's and (to some extent) ESLint's job is to find problematic code and warn you.

Does the engine warn against potentially code?

Sometimes! For instance, most engines will warn you when rejected Promises remain uncaught.

Why don't they for this case?

I can't speak for JavaScript engine manufacturers, but I can potentially imagine that if they would implement a warning / some behavior against your pet issue is going to be based on a number of factors, including how likely it is that this is a problem, how easy it is to detect and what's the cost in the engine to try and figure out if something bad is happening. Nothing is free and JavaScript engines need to be fast.

In my opinion this is an unlikely mistake for someone to make, and if the mistake is made it's quickly noticed because of how bad the effects are. This doesn't feel like the case where handholding is needed.

But even if this is a common problem, I feel ESLint is probably still a better place for something to detect and warn you.

Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
Evert
  • 93,428
  • 18
  • 118
  • 189
  • 4
    What questions are you answering? The quoted text is not from the question, in any of its revisions... – Heretic Monkey Jul 08 '22 at 20:41
  • @HereticMonkey my attempt was to answer a more general question.. e.g.: why doesn't my javascript engine do something about my clearly broken code. – Evert Jul 08 '22 at 21:12
  • "most engines will warn you when rejected Promises remain uncaught." That's a specs requirement not an "engine" feature. This answer conflates a lot of unrelated ideas in very confusing ways. – Kaiido Jul 08 '22 at 23:19