Skip to main content

Command Palette

Search for a command to run...

Understanding Event Loop via a Scheduler

Updated
3 min read
Understanding Event Loop via a Scheduler
S

I'm Shubham (@shubhamsinghbundela), I'm a Software Engineer, a Full-stack developer, a tech enthusiast, and a technical writer here on @Hashnode. I have a strong zeal to share my acquired knowledge and I am also willing to learn from others.

While solving a Time-Sliced Task Scheduler problem, I realized that my understanding of the event loop, microtasks, and macrotasks was wrong.

await does not automatically yield control to the event loop.


Problem Statement:

  • We need to build a scheduler that ensures no single task blocks the event loop for too long.

  • Instead of running tasks completely, each task should execute in small parts and pause in between.

  • This pause (yield) allows other tasks, including newly added or higher-priority ones, to run.

  • Since JavaScript cannot stop tasks forcefully, tasks must voluntarily give up control.
    This concept is called cooperative multitasking, commonly used in UI frameworks to keep applications responsive.


Implementation

class TimeSlicedScheduler {
  constructor() {
    this.queue = [];
  }

  schedule(task) {
    this.queue.push(task);
  }

  async run() {
    while (this.queue.length) {
      const task = this.queue.shift();

      await task();

      // Yield control
      await new Promise(resolve => setTimeout(resolve, 0));
    }
  }
}

Key Test Case

const runPromise = scheduler.run();

await new Promise(r => setTimeout(r, 0));
events.push("event-loop");

await runPromise;

What Happens WITHOUT Yield

If we remove the yield:

await task();

//remove yield

Execution Flow:

  1. task-1 runs

  2. Immediately continues

  3. task-2 runs

  4. Then event loop runs

Output:

task-1
task-2
event-loop ❌

Why This Happens

Because await uses microtasks, and:

Microtasks run immediately after the current execution, before moving to the next event loop cycle.

So execution continues without any pause.


What Changes WITH Yield

await task();

// added yield
await new Promise(resolve => setTimeout(resolve, 0));

This introduces a macrotask, which:

  • Pauses execution

  • Gives control back to the event loop

  • Allows other pending work to run


Execution Flow WITH Yield

  1. task-1 runs

  2. Scheduler pauses (setTimeout)

  3. Event loop executes "event-loop"

  4. Scheduler resumes

  5. task-2 runs

Output:

task-1
event-loop
task-2

Microtask vs Macrotask (Simple View)

Type Examples Behavior
Microtask await, Promise.then Runs immediately
Macrotask setTimeout Runs in next cycle

Key Insight

Microtasks don’t give control back to the event loop — macrotasks do.


Final Takeaway

  • await alone is not enough for yielding

  • You need a macrotask (setTimeout) to pause execution

  • This pattern is used in real-world systems to avoid blocking


One Line to Remember

If you want to truly yield in JavaScript, use a macrotask — not just await.

This small detail completely changed how I think about async execution in JavaScript.

More from this blog

Shubham Tech. Blog's

55 posts

Problem Solver | Currently Working As a Full Stack Developer, Community Leader At @Dev_Matrix | Previously Contributor at @RealDevSquad, @TeamShiksha