NodeJSAsynchronous Programming in Node

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

Text
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

JS
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
Blocking the one thread blocks *everything*
Because there's a single thread for your JavaScript, a synchronous operation — `readFileSync`, a giant `for` loop, `JSON.parse` of a huge payload — freezes the *entire* process. No requests are served, no timers fire, no callbacks run until it returns. Sync APIs are fine in scripts and startup code; in a server's hot path they're poison. See [Blocking vs Non-Blocking Code](/nodejs/blocking-vs-nonblocking).
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

Text
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 callback
Node is great at I/O, not at CPU crunching
This architecture makes Node superb for *I/O-bound* work — APIs, proxies, real-time apps — where the job is mostly shuffling data and waiting on the network or disk. It's a poor fit for *CPU-bound* work (video encoding, big computations) that monopolizes the single thread. For that, use `worker_threads` or a different tool.
Three generations of async syntax

Style

Looks like

Era

Callbacks

fn(args, (err, result) => {…})

Original Node convention

Promises

fn(args).then(r => …).catch(…)

ES2015

async / await

const r = await fn(args)

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

JS
// 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)
}
Next
See the machine that orchestrates all of this, cycle by cycle: [The Event Loop in Depth](/nodejs/event-loop).