Skip to content

Asynchronous Node.js

Asynchronous programming is the single most important concept in Node.js. This lesson explains what "asynchronous" actually means in practice and sets up the callbacks, promises, async/await, and event loop lessons that follow.

Why Node.js Is Asynchronous by Design

Most I/O operations, reading a file, querying a database, calling another API, take time to complete relative to CPU speed. A synchronous approach would block the entire program while waiting. Node.js instead starts the operation, immediately continues running other code, and gets notified later when the result is ready.

This non-blocking model is why a single Node.js process can serve thousands of concurrent requests: while one request waits on a database query, the same thread is free to start handling other requests instead of sitting idle.

console.log('1: Request received');

setTimeout(() => {
  console.log('3: Database result ready (simulated)');
}, 1000);

console.log('2: Continuing to handle other work');

The output prints 1, 2, 3, not 1, 3, 2. Node.js doesn't wait for the 1-second timer, it moves on immediately and comes back once the delay elapses.

Three Ways to Write Async Code

// 1. Callbacks
fs.readFile(path, (err, data) => { ... });

// 2. Promises
fs.promises.readFile(path).then((data) => { ... });

// 3. async/await
const data = await fs.promises.readFile(path);
  • Callbacks are the original async pattern in Node.js, a function passed in to be called later.
  • Promises represent a value that will be available eventually, with .then()/.catch().
  • async/await is syntax sugar over promises that reads like synchronous code.
  • All three approaches ultimately rely on the same event loop underneath.

Async Node.js Cheatsheet

A quick comparison of the async patterns covered in the next few lessons.

Pattern Example Introduced
Callback fn(arg, (err, result) => {}) Node.js's original style
Promise fn(arg).then(result => {}) ES2015
async/await const result = await fn(arg); ES2017
Event emitter emitter.on('event', handler) Node.js core pattern
Timers setTimeout(fn, ms) Classic async scheduling

Blocking vs Non-Blocking Code

"Blocking" code halts further execution until an operation finishes; "non-blocking" code starts the operation and immediately continues, handling the result later through a callback, promise, or await. Node.js's core APIs default to non-blocking versions wherever possible, with blocking (*Sync) alternatives available for scripts and startup code.

  • Blocking: fs.readFileSync(), halts execution until the file is fully read.
  • Non-blocking: fs.readFile() / fs.promises.readFile(), returns immediately and resolves later.
  • CPU-bound synchronous code (like a heavy loop) blocks regardless of which I/O style you use elsewhere.

Why This Matters at Scale

Imagine 1,000 simultaneous requests, each waiting 200ms on a database query. A blocking model would need 1,000 threads (or serve requests one at a time, taking 200 seconds total). Node.js's non-blocking model lets one thread issue all 1,000 queries almost immediately and respond to each as its result comes back, dramatically improving throughput for I/O-heavy workloads.

Common Mistakes

  • Writing synchronous, blocking code (*Sync methods, heavy loops) inside request handlers of a running server.
  • Assuming asynchronous code runs in a different thread, most of it runs on the same main thread, just not in blocking order.
  • Mixing callbacks, promises, and async/await inconsistently across a codebase, hurting readability.
  • Forgetting that asynchronous operations can resolve in a different order than they were started.

Key Takeaways

  • Node.js defaults to non-blocking I/O so a single thread can serve many concurrent operations.
  • Callbacks, promises, and async/await are three different syntaxes for the same underlying async model.
  • Blocking, synchronous code should be reserved for startup scripts, not request handlers.
  • Non-blocking I/O is what makes Node.js efficient for I/O-heavy workloads like APIs and real-time apps.

Pro Tip

When in doubt about whether an operation is blocking, check whether it has a Sync suffix or explicitly returns a promise/accepts a callback. If it's neither, it's very likely synchronous and can block the event loop.