Course:Node.js & Express/
Lesson

Every Node.js developer encounters code that behaves in unexpected order. A setTimeout with 0 delay runs after code that comes later in the file. A database query returns data out of sequence. A server freezes because one request is doing heavy computation. All of these trace back to the same mechanism: the event loopWhat is event loop?The mechanism that lets Node.js handle many operations on a single thread by delegating slow tasks and processing their results when ready.. Understanding it is the single most important concept for reading and debugging Node.js code that AI generates.

The mental model

Think of the event loopWhat is event loop?The mechanism that lets Node.js handle many operations on a single thread by delegating slow tasks and processing their results when ready. as a chef working alone in a kitchen. The chef can only do one thing at a time (single-threadedWhat is single-threaded?A model where one main execution thread handles all work - Node.js uses this with an event loop to handle many requests concurrently.), but instead of standing at the oven watching food cook, the chef starts the oven, moves on to chop vegetables, sets a timer for the pasta, and checks back on each task when it signals that it is ready.

Node.js works the same way. When your code starts an async operation (read a file, query a database, make an HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. request), Node.js hands that operation off to the operating system or a background thread pool, then immediately moves on to the next line of code. When the operation completes, a callbackWhat is callback?A function you pass into another function to be called later, often when an operation finishes or an event occurs. is placed in a queue, and the event loop picks it up on the next cycle.

console.log('1: Start');

setTimeout(() => {
  console.log('2: Timeout callback');
}, 0);

Promise.resolve().then(() => {
  console.log('3: Promise callback');
});

console.log('4: End');

Output:

1: Start
4: End
3: Promise callback
2: Timeout callback

This surprises most beginners. Even with a 0ms delay, the setTimeout callback runs last. The event loop has a specific order it follows.

02

The phases of the event loopWhat is event loop?The mechanism that lets Node.js handle many operations on a single thread by delegating slow tasks and processing their results when ready.

The event loop cycles through phases in a fixed order. Each phase has a queue of callbacks to process.

PhaseWhat runsExamples
SynchronousAll regular code, top to bottomconsole.log(), variable assignments, function calls
MicrotasksPromise callbacks, queueMicrotask().then(), await continuations, process.nextTick()
TimerssetTimeout and setInterval callbacksDelayed execution
I/O callbacksFile system, network, database callbacksfs.readFile callback, HTTP response handlers
ChecksetImmediate() callbacksRuns after I/O phase

The critical rule: microtasks always run before the next phase. After every phase, Node.js drains the microtask queue completely before moving on. This is why Promises resolve before setTimeout, Promises create microtasks, which have priority over timer callbacks.

// This demonstrates phase ordering
setImmediate(() => console.log('setImmediate'));
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));

// Output:
// nextTick      (microtask - highest priority)
// Promise       (microtask)
// setTimeout    (timer phase)
// setImmediate  (check phase)

You do not need to memorize every phase. What you need to know is this: synchronous code runs first, then microtasks (Promises), then everything else. That covers 95% of the ordering questions you will encounter.

03

Why async/awaitWhat is async/await?A syntax that lets you write asynchronous code (like fetching data) in a readable, step-by-step style instead of chaining callbacks. is not parallel

This is where AI-generated code frequently misleads. async/await makes asynchronous code look synchronous, but it does not make things run in parallel.

// Sequential - takes ~2 seconds total
async function fetchBoth() {
  const users = await fetch('/api/users');       // waits ~1s
  const posts = await fetch('/api/posts');       // waits ~1s after users finish
  return { users, posts };
}

// Parallel - takes ~1 second total
async function fetchBoth() {
  const [users, posts] = await Promise.all([
    fetch('/api/users'),                         // starts immediately
    fetch('/api/posts'),                         // starts immediately
  ]);
  return { users, posts };
}
AI pitfall
AI almost always generates sequential await calls when the operations are independent. If you see two await statements in a row where the second does not depend on the first, the code is slower than it needs to be. Wrap independent operations in Promise.all().

The difference between non-blocking and parallel is critical:

  • Non-blocking means Node.js does not freeze while waiting for an operation. It can handle other requests.
  • Parallel means multiple operations execute at the exact same time.

async/await gives you non-blocking behavior. Promise.all() gives you parallel execution of multiple non-blocking operations.

04

Blocking the event loopWhat is event loop?The mechanism that lets Node.js handle many operations on a single thread by delegating slow tasks and processing their results when ready.

The single most dangerous mistake in Node.js is blocking the event loop. When synchronous code takes a long time (reading a large file synchronously, running a complex calculation, parsing a huge JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. string), the entire server freezes. No other requests can be handled until the blocking operation finishes.

// DANGEROUS: blocks the event loop for every request
app.get('/api/data', (req, res) => {
  const data = fs.readFileSync('huge-file.json', 'utf-8');  // blocks!
  const parsed = JSON.parse(data);                           // blocks!
  res.json(parsed);
});

// CORRECT: non-blocking version
app.get('/api/data', async (req, res) => {
  const data = await fs.promises.readFile('huge-file.json', 'utf-8');
  const parsed = JSON.parse(data);
  res.json(parsed);
});

The synchronous version works fine in development with one user. In production with 100 concurrent users, one request reading a large file blocks all 99 other requests from being processed.

OperationBlocking versionNon-blocking version
Read filefs.readFileSync()fs.promises.readFile() / await
Crypto hashcrypto.pbkdf2Sync()crypto.pbkdf2() with callback
JSON parseJSON.parse(hugeString)Stream parsing with JSONStream
Child processexecSync()exec() or spawn()
AI pitfall
AI frequently generates readFileSync and other sync methods in server code. In a script that runs once (a build tool, a CLI command), synchronous operations are fine. In a server handling concurrent requests, they are a performance disaster. Always check whether AI used sync or async versions.
05

setTimeout is not what you think

setTimeout(fn, 100) does not mean "run this function in exactly 100ms." It means "run this function no sooner than 100ms, whenever the event loopWhat is event loop?The mechanism that lets Node.js handle many operations on a single thread by delegating slow tasks and processing their results when ready. gets around to it." If synchronous code or other callbacks are still running, the timer fires late.

const start = Date.now();

setTimeout(() => {
  console.log(`Fired after ${Date.now() - start}ms`);
  // Might print: "Fired after 105ms" or "Fired after 250ms"
  // depending on what else is happening
}, 100);

// Simulate blocking work
let sum = 0;
for (let i = 0; i < 1e8; i++) sum += i;

If the blocking loop takes 200ms, the timeout fires after 200ms, not 100ms. The timer is a minimum delay, not a guarantee. This matters for anything time-sensitive: rate limiters, tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. expiration, animation frames.

06

Practical rules

These rules will help you read and review Node.js code effectively:

  1. Never use sync methods in server code. If the method name ends in Sync, it blocks.
  2. Use Promise.all() for independent async operations. Sequential await is slower.
  3. Offload CPU work. If a function does heavy computation, move it to a worker thread or a separate service.
  4. Do not trust timer precision. setTimeout gives you minimum delay, not exact delay.
  5. Microtasks before macrotasks. Promises resolve before setTimeout, always.