My initial point what that your terms are muddled.
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.)
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.
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.)