Event Loop & Timers

The Course
•
4 min read

Finish the current work before scheduled callbacks

JavaScript runs a piece of synchronous code to completion before it starts another scheduled callback in the same environment. Promise reactions and await continuations use a microtask queue. When current work yields, queued microtasks run before the browser moves to the next timer task. A timer’s delay is a minimum scheduling threshold, not an exact appointment.

setTimeout(callback, milliseconds) schedules a callback and immediately returns a timer handle. clearTimeout(handle) cancels a still-pending timer. Passing zero does not mean “call the callback now.” Avoid using arbitrary sleeps to wait for a request: wait for the request’s actual promise.

new Promise((resolve, reject) => { ... }) adapts a callback-style operation. Its executor runs immediately; resolve fulfills the promise and reject rejects it. Use this when adapting a timer or event, not to wrap every async function in another promise. Await already works on a function returning a promise.

The example joins the timer promise explicitly so the editor knows when the demonstration is complete. In application code, leave long-running work cancellable and avoid blocking loops that prevent other callbacks from running.

Predict and trace

Write the output before running. Follow the values through each call; if your prediction differs, identify the first step that surprised you.

Predict, then run JavaScript
async function main() {
  const timer = new Promise(resolve => {
    setTimeout(() => {
      console.log("timer");
      resolve();
    }, 0);
  });
  const reaction = Promise.resolve().then(() => {
    console.log("promise");
  });

  console.log("sync");
  await reaction;
  await timer;
}

main();
Check your prediction
sync
promise
timer

The timer cannot interrupt the current synchronous log. The promise reaction runs at the microtask checkpoint before the timer task.

A zero-delay timer still waits for a turn

Synchronous statements finish before promise reactions and timer callbacks can run. A delay of zero requests a later opportunity; it does not interrupt the current call. Treat timer delays as minimum scheduling requests rather than precise deadlines.

Await pauses its async function, not every other operation. A caller can continue after the async function reaches its first await. To collect all intended output in this editor, return and await the actual work rather than launching it and forgetting it.

A zero-delay timer still waits for a turn: trace JavaScript
async function main() {
  const work = Promise.resolve().then(() => console.log("reaction"));

  console.log("registered");
  await work;
  console.log("awaited");
}

main();
Check your prediction
registered
reaction
awaited

Registering then does not run its callback inline. The statement registered appears before the queued reaction. Awaited is printed only after that particular promise settles.

A zero-delay timer still waits for a turn: contrast JavaScript
async function main() {
  async function child() {
    console.log("child starts");
    await Promise.resolve();
    console.log("child resumes");
  }

  const pending = child();

  console.log("caller continues");
  await pending;
}

main();
Check your prediction
child starts
caller continues
child resumes

The child runs synchronously until its first await, then yields to its caller. Awaiting pending afterward keeps the caller responsible for completion.

Try the rule on new inputs

Add a log before calling child and another after awaiting it. Predict all five positions. Explain why awaiting Promise.resolve once is not a general method for flushing an arbitrary chain of asynchronous operations.

Build it yourself

Exercise

Event Loop & Timers: practise

Write delayValue(value), returning a promise that fulfills with value from a timer. Print “Waiting”, await delayValue(“Ready”), then print the returned value.

Your working space JavaScript
// Use a short timer; the exact elapsed time is not the contract.
Hint 1: the idea

Return a promise that resolves from the timer callback.

Hint 2: a first step

The resolve function accepts the fulfillment value.

Show one solution
async function main() {
  function delayValue(value) {
    return new Promise(resolve => {
      setTimeout(() => resolve(value), 0);
    });
  }

  console.log("Waiting");
  console.log(await delayValue("Ready"));
}

main();
Waiting
Ready

Add a synchronous log immediately after creating the promise and before awaiting it. Predict its order. Do not assert that the zero-delay timer took exactly zero milliseconds.

Test a changed requirement

Add a synchronous log immediately after creating the promise and before awaiting it. Predict its order. Do not assert that the zero-delay timer took exactly zero milliseconds.

Explain before moving on

What runs immediately inside a Promise constructor? Why can a busy synchronous loop delay a timer?

Was this article helpful?

Your response is saved on this device.