Node.js Callbacks, Non-Blocking I/O, and Callback Hell
Understand why synchronous code blocks Node.js, how callbacks enable non-blocking I/O with fs.readFile, and why callback hell led to promises and async/await.
May 28, 2026
Node.js Callbacks, Non-Blocking I/O, and Callback Hell
In Node.js, understanding blocking vs non-blocking code is very important.
If you get this concept right, you’ll understand why callbacks, promises, and async/await exist.
In this post, we’ll cover:
Why synchronous code is blocking
Why that is a problem in Node.js
How callbacks can be synchronous or asynchronous
How async callbacks make I/O non-blocking
What happens internally with fs.readFile
How callback hell happens
Why promises and async/await are better alternatives for complex flows
1) Synchronous code is blocking
Synchronous operations run line by line, and each step waits for the previous one to finish.
If one operation takes a long time, everything after it must wait.
That blocking behavior is especially problematic in server runtimes.
2) Why blocking is a bigger issue in Node.js
Node.js handles JavaScript execution on a single main thread (event loop thread) per process.
So many client requests can depend on that same thread’s responsiveness.
If one request runs a blocking operation on the main thread, other requests can get delayed.
A practical way to think about it:
One thread orchestrates JS execution for all incoming work in that process
If that thread is blocked, everyone waits
(Internally, Node.js can use worker threads in libuv for certain operations, but JS callback execution returns to the main event loop thread.)
3) Callbacks can be synchronous or asynchronous
A callback is just a function you pass to another function so it can be invoked from there.
That does not mean every callback runs later or in the background.
Some APIs call your callback immediately and many times on the same thread, before the outer function returns.
const nums = [3, 1, 4, 2];
nums.sort((a, b) => b - a);
console.log(nums); // [4, 3, 2, 1]
Here, (a, b) => b - a is a comparator callback. sort calls it repeatedly while comparing elements. Each call runs synchronously inside sort. When sort returns, sorting is already complete.
Other common synchronous callback examples:
array.map((item) => ...)
array.filter((item) => ...)
array.forEach((item) => ...)
The key distinction
Synchronous callback
Asynchronous callback
When it runs
Before the parent function returns
After the parent function returns (often when I/O finishes)
Example
nums.sort((a, b) => b - a)
fs.readFile(path, 'utf-8', (err, data) => ...)
Blocking?
Yes, while the parent runs (e.g. sorting blocks until done)
The parent returns quickly; work continues in the background
So callback and non-blocking are not the same thing. Many JavaScript APIs use callbacks without any async I/O at all. Node’s reputation for callbacks is mostly about the asynchronous style used for files, networks, and databases.
4) Non-blocking I/O with callbacks (fs.readFile)
To avoid blocking during slow work (like disk reads), Node.js uses asynchronous APIs like fs.readFile.
fs.readFile('<path>', 'utf-8', (err, data) => {
console.log(data);
});
console.log('reading'); // this prints first
console.log('reading') prints first because readFile is non-blocking.
Node registers your callback and continues executing other code instead of waiting for file reading to finish.
5) What happens behind the scenes
At a high level:
Your app calls fs.readFile(...)
Node hands the file-read task to a background worker (via libuv thread pool / OS async facilities)
Main thread keeps running other tasks
When file read finishes, Node queues your callback
Event loop runs callback and passes (err, data)
So yes—your callback is stored and invoked later when the operation completes.
That is the core idea of non-blocking I/O.
6) Why callback hell happens
Callbacks are fine for one operation.
Problems start when many dependent async steps are nested inside each other.
async/await is built on top of Promises and is generally the easiest style to read because it looks very similar to synchronous code while remaining non-blocking.
Evolution of asynchronous code in Node.js
Callbacks
↓
Promises
↓
async/await
Callbacks introduced non-blocking I/O, Promises improved readability and error handling, and async/await made asynchronous code feel much more natural to write and maintain.
Final thoughts
Node.js non-blocking design is one of its biggest strengths.
Use asynchronous APIs (fs.readFile, network calls, DB calls) to keep the event loop responsive. Remember that callbacks appear in everyday sync APIs too (like sort and map). For multi-step async logic, prefer promises or async/await over deeply nested callbacks.