Blocking vs Non-Blocking I/O
Every time your code reads a file or talks to a database, it has to wait for the result. What your program does while it waits is the difference between blocking and non-blocking I/O.
Definition
- Blocking I/O: the main thread stops and waits until the I/O finishes. Nothing else runs in the meantime.
- Non-blocking I/O: the main thread starts the I/O, hands it to libuv, and moves on. A callback runs later, when the result is ready.
I/O (Input/Output) means any work outside your program: reading files, network requests, database queries.
Analogy: think of ordering coffee.
- Blocking: you order and stand at the counter until your coffee is ready. The line behind you can't move.
- Non-blocking: you order, get a token, and step aside. The next customer orders, and your name is called when the coffee is ready.
| Blocking | Non-Blocking | |
|---|---|---|
| Main thread | Waits | Keeps running |
| Result comes back | As the return value | In a callback, promise, or await |
| Node.js example | fs.readFileSync | fs.readFile, fs.promises.readFile |
| Other requests | Must wait | Handled in the meantime |
| Naming hint | Methods ending in Sync | Methods without Sync |
Blocking Example
const fs = require("fs");console.log("Start");const data = fs.readFileSync("data.txt", "utf8"); // waits hereconsole.log("File read");console.log("End");// Output: Start, File read, End
The program stops at readFileSync until the whole file is read. End cannot print until then.
Non-Blocking Example
const fs = require("fs");console.log("Start");fs.readFile("data.txt", "utf8", (err, data) => {console.log("File read");});console.log("End");// Output: Start, End, File read
readFile hands the work to libuv (it runs on the thread pool), and the main thread moves on. File read prints later, when the event loop runs the callback.
Why It Matters on a Server
A Node.js server handles every request on one main thread. One blocking call makes every user wait.
const http = require("http");const fs = require("fs");// Blocking: each request freezes the server while the file is readhttp.createServer((req, res) => {const data = fs.readFileSync("big-file.txt");res.end(data);}).listen(3000);// Non-blocking: other requests are handled while the file is readhttp.createServer((req, res) => {fs.readFile("big-file.txt", (err, data) => {res.end(data);});}).listen(3001);
If reading the file takes 100 ms and 10 users arrive at once:
| Server | What happens |
|---|---|
| Blocking | Requests are served one after another, so the last user waits about 1 second |
| Non-blocking | All 10 reads start almost at once, and each user waits about 100 ms |
Three Ways to Write Non-Blocking Code
All three are non-blocking. Only the style is different.
const fs = require("fs");const fsp = require("fs/promises");// 1. Callbackfs.readFile("data.txt", "utf8", (err, data) => {if (err) return console.error(err);console.log(data);});// 2. Promisefsp.readFile("data.txt", "utf8").then((data) => console.log(data)).catch((err) => console.error(err));// 3. async / await (recommended)async function read() {try {const data = await fsp.readFile("data.txt", "utf8");console.log(data);} catch (err) {console.error(err);}}
Note:
awaitlooks like blocking code, but it only pauses that async function. The main thread is free to run other code.
Mistake: Mixing Blocking and Non-Blocking
const fs = require("fs");// Wrong: the file may be deleted before it is readfs.readFile("temp.txt", "utf8", (err, data) => {console.log(data);});fs.unlinkSync("temp.txt"); // runs immediately// Right: delete only after the read has finishedfs.readFile("temp.txt", "utf8", (err, data) => {console.log(data);fs.unlink("temp.txt", () => {});});
Non-blocking code does not wait, so any step that depends on the result must go inside the callback (or after await).
When Is Blocking Code Okay?
| Okay | Avoid |
|---|---|
| Loading a config file at startup, before the server starts | Inside request handlers |
| Small CLI scripts with one user | In anything that serves many users |
| One-time setup tasks | In loops or frequently called functions |
Summary
- Blocking I/O makes the main thread wait. Non-blocking I/O lets it keep running.
- In Node.js, methods ending in
Syncare blocking. - Non-blocking I/O gives its result through a callback, promise, or
await. - On a server, one blocking call delays every user, because there is only one main thread.
awaitpauses only its own async function, not the main thread.- Put code that depends on the result inside the callback or after
await. - Blocking code is fine at startup and in simple scripts, but never in request handlers.
Interview Answer: What Is the Difference Between Blocking and Non-Blocking I/O?
Blocking I/O stops the main thread until the operation finishes, so nothing else can run in the meantime. In Node.js, methods ending in Sync, such as fs.readFileSync, are blocking.
Non-blocking I/O starts the operation, hands it to libuv, and lets the main thread continue. The result comes back later through a callback, a promise, or await, and the event loop runs it when the main thread is free.
Since Node.js runs JavaScript on a single thread, one blocking call in a request handler makes every user wait. So I use non-blocking APIs like fs.promises with async/await in servers, and I use sync methods only for one-time tasks like loading config at startup.
The next topic is worker threads.