Event-Driven Programming
Node.js is event-driven at its core. Instead of your program asking "is the data ready yet?" in a loop, it registers interest in things that will happen — a request arrives, a file finishes reading, a timer fires — and lets the runtime call back when they do. This single architectural choice is why a single-threaded Node process can juggle thousands of simultaneous connections.
Polling vs events
Picture checking your mailbox. Polling is walking outside every five minutes to see if mail came — wasteful, and you're not doing anything else while you check. Event-driven is a doorbell: you do other work, and the bell tells you the instant something arrives:
Two ways to wait
POLLING (blocking, wasteful):
while (!data.ready) { /* spin, doing nothing useful */ }
process(data)
EVENT-DRIVEN (Node's model):
source.on('data', process) // "call me when it's ready"
// ...meanwhile, handle other workThe three roles
Role | What it does | Example |
|---|---|---|
Event source / emitter | Announces that something happened | An |
Event | A named occurrence, optionally with data |
|
Listener / handler | A function run in response | Your |
In Node these are unified by the EventEmitter class — the mechanics live in The events Module. Almost every async-capable object you'll meet is an emitter.
It is everywhere in Node
import { createServer } from 'node:http'
const server = createServer()
// React to events instead of asking "did a request come?"
server.on('request', (req, res) => res.end('hi'))
server.on('connection', (socket) => console.log('new TCP connection'))
server.on('close', () => console.log('server stopped'))
server.listen(3000, () => console.log('listening')) // 'listening' is an event tooWhy this enables high concurrency
Because listeners only run when their event fires, the single main thread is never stuck waiting. While a database query is in flight, the thread is free to accept new requests. The runtime — via the event loop — dispatches each callback as its event becomes ready:
One thread, many in-flight operations
Request A → start DB query ──┐ (waiting, thread is FREE)
Request B → start file read ─┤ (waiting, thread is FREE)
Request C → start API call ──┘ (waiting, thread is FREE)
│
DB result ready ──────────┘→ run A's handler
File ready ───────────→ run B's handler
API responds ───────────→ run C's handlerThe mental shift
Stop thinking "do X, then wait for Y, then do Z" — think "when Y happens, do Z".
Your code mostly registers reactions; the runtime decides when they run.
Order is driven by when events fire, not by line order — embrace it rather than fighting it.
Keep handlers small and fast; offload heavy work so the loop stays responsive.