Skip to content
Preptima

Node.js and TypeScript fundamentals

Where the event loop actually spends its time, what blocks it and what does not, how the two module systems collide, what TypeScript's type system does and does not check at runtime, and the packaging details that only break in production. Fifty-seven items, twelve worked through with code or a diagram.

57 questions

Go deeper on Node.js & TypeScript
Standard Sheet View

The event loop and runtime model

Show me the phases of the event loop and where things run.

Node's loop is not one queue. It cycles through phases, and knowing which phase a callback belongs to explains most ordering surprises.

Between every phase transition — and after each individual callback — the microtask queues drain completely: process.nextTick first, then promise callbacks. That is why an await resumes before any timer, no matter how short the timer.

The poll phase is where a mostly-idle server spends its life, blocked in the kernel waiting for a socket to become readable. This is the crucial point about Node's model: waiting for I/O costs nothing, because the thread is parked in the operating system rather than spinning.

setImmediate runs in the check phase, so it fires after the current poll phase completes; setTimeout(fn, 0) waits for the next timers phase. Despite the names, setImmediate is generally the sooner of the two, which is exactly backwards from what the names suggest.

process.nextTick is not a phase at all. It runs before promise callbacks and before the loop advances, so recursive nextTick calls starve the loop completely — no I/O, no timers, nothing. It exists for library internals that must run before anything else, and application code almost always wants a promise.

What actually blocks the event loop?

CPU work and synchronous system calls, because there is one thread running your JavaScript. A tight loop over a large array, JSON.parse on a twenty-megabyte payload, crypto.pbkdf2Sync, fs.readFileSync, a catastrophically backtracking regular expression — each stops every other request in the process, including the health check, which is why the orchestrator then kills a server that was merely busy. Waiting on a database, by contrast, blocks nothing at all.

Show me the exact ordering of the scheduling primitives.

This is the question that distinguishes people who have debugged an ordering bug from people who have read that Node is asynchronous.

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('sync');

// sync, nextTick, promise, timeout, immediate

Synchronous code first, always — nothing asynchronous interleaves with a running stack.

Then the microtask queues, in their fixed order: the nextTick queue drains entirely, then the promise queue. That ordering is why a nextTick scheduled inside a promise callback still runs before the next promise callback.

Then the loop advances. Here the output is mildly unstable on the first tick — the timeout and immediate order can swap depending on how long the process took to start, because the timer may or may not have expired by the time the timers phase is reached. Inside an I/O callback it is deterministic: setImmediate always wins, because check follows poll in the same iteration while timers require a full lap.

The practical rule this leaves you with: use setImmediate to yield to I/O between chunks of work, and avoid process.nextTick in application code entirely, because starving the loop is a failure mode with no error message.

What is `AsyncLocalStorage` for?

Carrying context — a request id, a tenant, a user — through an asynchronous call chain without threading it through every function signature. In a synchronous language a thread-local would do it; in Node the call stack is discarded at every await, so there is no ambient place to put it. AsyncLocalStorage hooks the async resource lifecycle to keep a store associated with the logical operation across those boundaries. It is what lets a logger attach the trace id automatically, and the cost is a measurable per-operation overhead plus the risk of leaking context into work that outlives the request.

How does Node do anything in parallel if it is single-threaded?

Only your JavaScript is single-threaded. File system operations, DNS lookups and some crypto run on a thread pool — four threads by default, which is a real and frequently-hit limit — and network I/O is handled by the operating system's event notification rather than by threads at all. So a service doing thousands of concurrent HTTP calls scales fine, while four concurrent pbkdf2 calls saturate the pool and a fifth waits.

When should you reach for a worker thread?

When the work is CPU-bound and must happen in-process: image manipulation, large parsing or serialisation jobs, compression, or a synchronous cryptographic operation on the request path. A worker has its own event loop and its own heap, so it cannot block the main one, and data crosses the boundary by copying unless you use a shared buffer. For work that does not need to be in-process, a separate queue and worker service is usually simpler to operate and to scale.

Why is clustering not the same as multithreading?

Because a cluster runs several separate processes, each with its own heap and event loop, sharing a listening socket. That gives you the cores without any shared-memory concurrency problems — and means nothing is shared, so in-memory caches, counters and sessions diverge per worker immediately. It is the same statelessness requirement as horizontal scaling, arriving on a single machine, and it catches people who assumed one process's memory was the whole application.