NodeJSEvent-Driven Programming

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

Text
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 work
The three roles

Role

What it does

Example

Event source / emitter

Announces that something happened

An http.Server

Event

A named occurrence, optionally with data

'request', 'data', 'close'

Listener / handler

A function run in response

Your (req, res) => {…}

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

JS
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 too
Callbacks, events, and promises are cousins
All three are forms of "tell me later". A *callback* is a one-shot function you pass in. An *event* is a named, repeatable callback you subscribe to. A *promise* is a one-shot future value. They interoperate — `events.once()` turns an event into a promise; `util.promisify` turns a callback into one. Node's async story (covered in the [Asynchronous Node.js](/nodejs/async-intro) section) is built on all three.
Why 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

Text
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 handler
The flip side: one slow listener blocks everyone
Event-driven concurrency works *only* if handlers return quickly. Because there's one thread, a listener that does heavy synchronous work — a giant loop, `JSON.parse` of a huge string, sync crypto — freezes the entire process: no other event can fire until it finishes. This is the [blocking-vs-non-blocking](/nodejs/blocking-vs-nonblocking) trap, and the reason CPU-heavy work belongs in worker threads.
The 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.

Next
Meet the class that makes all of this work, in depth: [The EventEmitter Class](/nodejs/event-emitter).