Promises are cooperative.
You cannot interrupt execution (unlike, say, preemptible threads).
You can only perform other execution when the current execution terminates or yields, e.g. with await.
---
I think you are wanting browser UI events to preempt between Promises/microtasks.
That's fair enough, but saying that Promise aren't cooperative is the wrong description.
You cannot split a large calculation in the middle by chaining promises to allow even other promises to make progress, let alone event loop processing.
As long as "can yield" means "able to yield, and able to not yield". More clearly put, the difference between preemptable and cooperative multitasking is
* A preempting scheduler interrupts execution without requiring cooperation from the task https://en.wikipedia.org/wiki/Preemption_(computing)
* A cooperative scheduler interrupts execution only when the task voluntarily yields control https://en.wikipedia.org/wiki/Cooperative_multitasking
> You cannot split a large calculation in the middle by chaining promises
You can split up large calculations easily:
(async() => {
console.log('1');
await (async() => {});
console.log('2');
})();
(async() => {
console.log('3');
await (async() => {});
console.log('4');
})();
prints interleaved 1, 3, 2, 4. Calculation split!---
HTML5 defines a scheduling system ("tasks") on top of ECMAScript's job ("microtask") system.
Thus, browsers have a tiered scheduling system. And DOM events happen at the higher task level.
In a browser, if you want to cooperatively schedule at the task level,
(async() => {
console.log('1');
await new Promise(resolve => setTimeout(resolve));
console.log('2');
})();
(async() => {
console.log('3');
await new Promise(resolve => setTimeout(resolve));
console.log('4');
})();
You could argue that HTML5 should not have created tiered queues. Perhaps you are correct, though I think it can come in handy.The question is how would you make sure 4 happens before 2?
Here's a real world example, from Node: You need to make 3 service calls, A B & C, to build a page. Service A is the fastest call, but takes a lot of processing time. Service C is the slowest call, but requires a bit of data from Service B. Since A and B are unrelated, odds are good they're being invoked from completely separate parts of the code.
If you fire A and B, service C won't get called until service A's processing is complete. You could await both A and B, then call C before you start the processing, but you have to turn your code flow inside out to do that, so it only works for trivial applications.
Adding promise chaining to A.process() won't get B's promise to resolve before A's chain finishes resolving. setImmediate() might work in some places, and you might be able to come up with a code pattern that works for your team, but I don't believe it's guaranteed to work everywhere.
Your hypothetical could benefit from a preemptable (aka non-cooperative) scheduler, which can forcibly interrupt A, to allow C to start.
A cooperative scheduler (which is what JS has) is at the mercy of A to properly yield.
---
As for how to yield on the macro- or microtask queue of your choice, they are the same difficulty to write.
// HTML5, Node.js
await new Promise(resolve => resolve());
await new Promise(resolve => setTimeout(resolve, 0));
// Node.js
await new Promise(resolve => resolve());
await new Promise(setImmediate);
You're correct that setTimeout and setImmediate are not guaranteed to work on all ES runtimes, because they are HTML5 and Node.js specific additions. (As is the entire concept of a separate macrotask queue, which you dislike so much.)
I’m tempted to call this the legacy of Brendan “design a language in a week” Eich, but that would let too many other people off the hook.
IMO, webasm can’t happen fast enough.