Asynchronous Programming in Node
Node runs your JavaScript on a single thread, yet it serves thousands of concurrent connections without breaking a sweat. The trick is asynchronous, non-blocking I/O: instead of waiting for a slow operation (a disk read, a network call) to finish, Node hands it off, keeps running other code, and comes back when the result is ready. This page builds the mental model the rest of the section depends on.
The problem async solves
I/O is slow compared to the CPU. Reading a file or querying a database takes milliseconds — during which a CPU could run millions of instructions. A synchronous, blocking approach wastes all of that time standing still:
Blocking vs non-blocking, one thread
BLOCKING — thread frozen during every wait:
read A [■■■■ wait ■■■■] done
read B [■■■■ wait ■■■■] done ← B can't start till A ends
NON-BLOCKING — overlap the waits:
read A [■■■■ wait ■■■■] done
read B [■■■■ wait ■■■■] done ← started immediately; waits overlap
(one thread, but it's never idle)Synchronous vs asynchronous code
import { readFileSync, readFile } from 'node:fs'
// SYNCHRONOUS — the thread STOPS here until the file is fully read
const data = readFileSync('big.txt', 'utf8')
console.log('after sync') // runs only after the read completes
// ASYNCHRONOUS — kick off the read, keep going immediately
readFile('big.txt', 'utf8', (err, data) => console.log('file ready'))
console.log('after async') // runs BEFORE 'file ready'after sync after async file ready
How one thread does many things at once
Node doesn't do the slow waiting itself. It delegates I/O to the operating system (and a small thread pool via libuv), then uses the event loop to pick up results as they arrive. Your JS thread only ever runs short bursts of your code — never the waiting:
Delegation, not waiting
Your JS thread OS / libuv (does the waiting)
───────────── ─────────────────────────────
readFile(cb) ──────────► start disk read
keep running…
keep running…
◄──────── read finished
run cb() ←── event loop schedules your callbackThree generations of async syntax
Style | Looks like | Era |
|---|---|---|
Callbacks |
| Original Node convention |
Promises |
| ES2015 |
async / await |
| ES2017 — the modern default |
All three describe the same underlying mechanism — "tell me when it's done". You'll meet each in depth: The Callback Pattern, Promises in Node.js, and async / await in Node.js.
The same task, three ways
// 1. Callback
readFile('f.txt', 'utf8', (err, data) => {
if (err) return console.error(err)
console.log(data)
})
// 2. Promise
readFileP('f.txt', 'utf8')
.then((data) => console.log(data))
.catch((err) => console.error(err))
// 3. async/await ← prefer this
try {
const data = await readFileP('f.txt', 'utf8')
console.log(data)
} catch (err) {
console.error(err)
}