Blocking vs Non-Blocking Code in Node.js

Node.js runs on single thread. Blocking code freezes whole server. Non-blocking code keep server alive. Difference = slow API vs fast API.
This about blocking, non-blocking, and async patterns in Node.js.
What Blocking Code Means
Blocking = waiting. Code wait for operation finish before continue.
Simple Definition
Blocking code pause execution until task complete. Nothing else happen. Like stuck in line at store.
Real Analogy
Restaurant waiter without non-blocking:
Waiter take order from customer 1.
Wait for kitchen finish cooking.
[Stand there 10 minutes doing nothing]
[Customer 2 wait, customer 3 wait, customer 4 wait]
Finally food ready.
Waiter bring food to customer 1.
NOW waiter take order from customer 2.
Bad service. Customers angry. Waiter waste time.
The Problem
// Blocking code
const fs = require('fs');
console.log('Start reading file');
const data = fs.readFileSync('large-file.txt'); // BLOCK HERE
console.log('File read complete');
console.log(data);
What happen:
1. Server start
2. Start reading file [WAIT]
3. [WAIT] [WAIT] [WAIT] <- Nothing happen for 5 seconds
4. File finally loaded
5. Print data
6. During step 3, if 100 users try access server, ALL wait
Why Blocking Kill Servers
One user read file = 5 seconds. 10 users read file = 50 seconds wait for user 10. 100 users? Server dead.
What Non-Blocking Code Means
Non-blocking = don't wait. Code start task, move on. When task done, callback fire.
Simple Definition
Non-blocking code start operation and continue. Task happen background. When done, callback tell code.
Real Analogy
Restaurant waiter WITH non-blocking:
Waiter take order from customer 1.
Send order to kitchen.
[Keep going - don't wait]
Waiter take order from customer 2.
Waiter take order from customer 3.
Kitchen finish customer 1 food.
Waiter bring food.
[Meanwhile take order from customer 4]
Kitchen finish customer 2 food.
Waiter bring food.
Good service. Customers happy. Waiter busy but move fast.
Code Example
// Non-blocking code
const fs = require('fs');
console.log('Start reading file');
fs.readFile('large-file.txt', (err, data) => {
// This callback run LATER when file ready
console.log('File read complete');
console.log(data);
});
console.log('Request sent, moving on');
What happen:
1. Server start
2. Start reading file (DON'T WAIT)
3. Move to next line immediately
4. "Request sent, moving on" print
5. File load in background
6. When ready, callback fire
7. Print data
During file read, server handle other users.
Why Blocking Slow Servers Down
Timeline Comparison
Blocking (synchronous):
Time (seconds)
0s ├─ User 1 come
├─ Read file [BLOCK]
5s ├─ File done
├─ User 2 come [wait in line]
├─ Read file [BLOCK]
10s ├─ File done
├─ User 3 come [wait in line]
├─ Read file [BLOCK]
15s └─ File done
Total time for 3 users = 15 seconds
Non-blocking (asynchronous):
Time (seconds)
0s ├─ User 1 come
├─ Start read file (don't wait)
0s ├─ User 2 come
├─ Start read file (don't wait)
0s ├─ User 3 come
├─ Start read file (don't wait)
5s └─ All 3 files ready, return data to users
Total time for 3 users = 5 seconds
Real Impact
With blocking:
- 100 users = 500 seconds (8+ minutes)
- Server crash or hang
With non-blocking:
- 100 users = 5 seconds
- Server smooth
Code Proof
// BLOCKING - slow
const fs = require('fs');
console.time('Blocking');
for (let i = 0; i < 3; i++) {
const data = fs.readFileSync('file.txt'); // Wait each time
console.log(`Read ${i}`);
}
console.timeEnd('Blocking');
// Blocking: 3000ms (each file take ~1 second)
// NON-BLOCKING - fast
const fs = require('fs');
console.time('NonBlocking');
for (let i = 0; i < 3; i++) {
fs.readFile('file.txt', (err, data) => {
console.log(`Read ${i}`);
});
}
console.timeEnd('NonBlocking');
// NonBlocking: 10ms (all start at once)
Async Operations in Node.js
Node.js have many built-in async functions. Don't use Sync version in production.
File Operations
Blocking (bad):
const fs = require('fs');
const data = fs.readFileSync('users.json'); // DON'T DO THIS
console.log(data);
Non-blocking (good):
const fs = require('fs');
fs.readFile('users.json', (err, data) => {
if (err) {
console.error('File error:', err);
return;
}
console.log(data);
});
Database Calls
Blocking (bad):
// Fake sync DB call (most real DB have this)
const user = db.getUserSync(1); // Block waiting for DB
console.log(user);
Non-blocking (good):
// Real async DB call
db.getUser(1, (err, user) => {
if (err) {
console.error('DB error:', err);
return;
}
console.log(user);
});
Network Requests
Blocking (impossible, no sync version):
// This don't exist - network always async
const response = http.requestSync(url); // Doesn't exist
Non-blocking (only way):
const http = require('http');
http.get(url, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log(data);
});
});
Callbacks: Simple Non-Blocking Pattern
Callback = function that run later.
What is Callback?
Callback = tell Node.js "When done, run this function".
fs.readFile('file.txt', (err, data) => {
// This function run LATER when file ready
console.log(data);
});
Function (err, data) => { ... } = callback.
Callback Pattern
function readUserFile(callback) {
fs.readFile('user.json', (err, data) => {
if (err) {
callback(err); // Pass error to callback
return;
}
callback(null, data); // Pass data to callback
});
}
// Use it
readUserFile((err, data) => {
if (err) {
console.error('Error:', err);
} else {
console.log('User:', data);
}
});
Callback Hell (Problem)
Nested callbacks get messy:
fs.readFile('users.json', (err, users) => {
if (err) console.error(err);
fs.readFile('posts.json', (err, posts) => {
if (err) console.error(err);
fs.readFile('comments.json', (err, comments) => {
if (err) console.error(err);
console.log(users, posts, comments);
// Deep nesting = hard to read
});
});
});
Solution = use Promises or async/await.
Promises: Better Non-Blocking Pattern
Promise = cleaner callback. Chain operations.
What is Promise?
Promise = object that represent future value.
const promise = fs.promises.readFile('file.txt');
promise
.then((data) => {
console.log('File read:', data);
})
.catch((err) => {
console.error('Error:', err);
});
Promise Chain
No nesting needed:
const fs = require('fs').promises;
fs.readFile('users.json')
.then((users) => {
console.log('Users loaded');
return fs.readFile('posts.json'); // Chain next operation
})
.then((posts) => {
console.log('Posts loaded');
return fs.readFile('comments.json');
})
.then((comments) => {
console.log('Comments loaded');
console.log('All data ready');
})
.catch((err) => {
console.error('Error:', err);
});
Clean. Easy read.
Promise Syntax
new Promise((resolve, reject) => {
// Do async work
if (success) {
resolve(data); // Pass data when done
} else {
reject(error); // Pass error if fail
}
});
Example:
function readFile(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
readFile('users.json')
.then((data) => console.log(data))
.catch((err) => console.error(err));
Async/Await: Cleanest Non-Blocking Pattern
Async/await = write async code like sync code.
What is Async/Await?
Async/await = Promises but easier read. Look like normal code.
async function loadData() {
try {
const users = await fs.promises.readFile('users.json');
console.log('Users:', users);
const posts = await fs.promises.readFile('posts.json');
console.log('Posts:', posts);
} catch (err) {
console.error('Error:', err);
}
}
loadData();
No .then() chains. Clean.
Async Function
async keyword make function return Promise:
async function getData() {
return 'some data'; // Auto wrapped in Promise
}
getData().then((data) => console.log(data)); // Work with Promise
Await Keyword
await pause function until Promise resolve:
async function loadFile() {
console.log('Start');
const data = await fs.promises.readFile('file.txt');
// Wait here until file ready
// Then continue
console.log('Done');
console.log(data);
}
loadFile();
Timeline:
1. Start (print)
2. Begin reading file
3. PAUSE (await wait)
4. File ready
5. Resume
6. Done (print)
7. Print data
Real Example: Async/Await
const express = require('express');
const fs = require('fs').promises;
const app = express();
app.get('/users/:id', async (req, res) => {
try {
const userId = req.params.id;
// Read user file
const userFile = await fs.readFile(`users/${userId}.json`);
const user = JSON.parse(userFile);
// Read user posts
const postsFile = await fs.readFile(`posts/${userId}.json`);
const posts = JSON.parse(postsFile);
// Return both
res.json({ user, posts });
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
app.listen(3000);
During await, server handle other users. Not blocked.
Blocking vs Non-Blocking: Side-by-Side
File Read Comparison
Blocking:
const fs = require('fs');
function readUser() {
// This freeze whole server
const data = fs.readFileSync('user.json');
return data;
}
const user = readUser();
console.log(user);
Non-blocking (Promise):
const fs = require('fs').promises;
function readUser() {
// Return Promise
return fs.readFile('user.json');
}
readUser()
.then((data) => console.log(data))
.catch((err) => console.error(err));
Non-blocking (Async/Await):
const fs = require('fs').promises;
async function readUser() {
try {
const data = await fs.readFile('user.json');
return data;
} catch (err) {
console.error(err);
}
}
const user = await readUser();
console.log(user);
Database Call Comparison
Blocking (bad):
const user = db.getUserSync(1); // Server freeze
res.json(user);
Non-blocking (good):
db.getUser(1, (err, user) => {
if (err) return res.status(500).json({ error: err });
res.json(user); // Server not frozen
});
Non-blocking (async/await, best):
async function handleRequest(req, res) {
try {
const user = await db.getUser(1);
res.json(user);
} catch (err) {
res.status(500).json({ error: err });
}
}
Real-World Examples
Example 1: Read File and Database
Wrong (blocking):
const fs = require('fs');
const db = require('./db');
app.get('/profile/:id', (req, res) => {
// Freeze server here
const settings = fs.readFileSync('config.json');
const user = db.getUserSync(req.params.id);
res.json({ user, settings });
});
Timeline for 3 users:
User 1: File read (2s) → DB query (3s) = 5s total
User 2: Wait... wait... wait... (5s)
User 3: Wait... wait... wait... (10s)
Right (non-blocking):
const fs = require('fs').promises;
const db = require('./db');
app.get('/profile/:id', async (req, res) => {
try {
// Both start at same time (parallel)
const [settings, user] = await Promise.all([
fs.readFile('config.json'),
db.getUser(req.params.id)
]);
res.json({ user, settings });
} catch (err) {
res.status(500).json({ error: err });
}
});
Timeline for 3 users:
User 1: Start both → Wait 3s → Done
User 2: Start both → Wait 3s → Done
User 3: Start both → Wait 3s → Done
Total: All 3 done in 3 seconds (not 15)
Example 2: Loop with Non-Blocking
Wrong (blocking):
const fs = require('fs');
function processFiles(filenames) {
const results = [];
for (const filename of filenames) {
// Read each file one by one (SLOW)
const data = fs.readFileSync(filename);
results.push(data);
}
return results;
}
processFiles(['file1.txt', 'file2.txt', 'file3.txt']);
// Take 3 seconds (1s each)
Right (non-blocking, parallel):
const fs = require('fs').promises;
async function processFiles(filenames) {
// Read all files at same time
const promises = filenames.map(filename =>
fs.readFile(filename)
);
const results = await Promise.all(promises);
return results;
}
await processFiles(['file1.txt', 'file2.txt', 'file3.txt']);
// Take 1 second (all run together)
Example 3: API Call with Non-Blocking
Wrong (blocking - would hang server):
// Blocking network call (doesn't exist in Node.js)
const data = http.requestSync(url); // NO
res.json(data);
Right (non-blocking):
const axios = require('axios');
app.get('/data', async (req, res) => {
try {
// Don't block - wait for response
const response = await axios.get('https://api.example.com/data');
res.json(response.data);
} catch (err) {
res.status(500).json({ error: err });
}
});
Server handle 100 users at once. Each await not block others.
Common Mistakes
Mistake 1: Using Sync Methods in Production
// ✗ Bad - freeze server
const data = fs.readFileSync('file.txt');
// ✓ Good - don't freeze
const data = await fs.promises.readFile('file.txt');
Mistake 2: Forgetting Error Handling
// ✗ Bad - error crash app
async function loadData() {
const data = await fs.readFile('file.txt');
console.log(data);
}
// ✓ Good - handle error
async function loadData() {
try {
const data = await fs.readFile('file.txt');
console.log(data);
} catch (err) {
console.error('Error:', err);
}
}
Mistake 3: Sequential When Could Be Parallel
// ✗ Bad - run one at time (3 seconds)
async function getData() {
const users = await db.getUsers();
const posts = await db.getPosts();
const comments = await db.getComments();
return { users, posts, comments };
}
// ✓ Good - run all together (1 second)
async function getData() {
return Promise.all([
db.getUsers(),
db.getPosts(),
db.getComments()
]);
}
Mistake 4: Callback Hell (Before Understanding Promises)
// ✗ Bad - nested and hard read
fs.readFile('file1.txt', (err, data1) => {
fs.readFile('file2.txt', (err, data2) => {
fs.readFile('file3.txt', (err, data3) => {
console.log(data1, data2, data3);
});
});
});
// ✓ Good - use Promise.all
async function readFiles() {
const [data1, data2, data3] = await Promise.all([
fs.promises.readFile('file1.txt'),
fs.promises.readFile('file2.txt'),
fs.promises.readFile('file3.txt')
]);
console.log(data1, data2, data3);
}
When to Block (Rare)
Generally: don't block.
But rare cases:
- Server startup - Load config before start
- CLI tools - Command line tool need blocking read
- One-time script - Not long-running server
// OK to block on startup
const config = fs.readFileSync('config.json');
const app = express();
app.use(/* setup */);
// NOW use async everywhere
app.get('/data', async (req, res) => {
const data = await getData();
res.json(data);
});
app.listen(3000);
Practice Assignment
1. Fix blocking code:
// Given blocking code, rewrite with async/await
const fs = require('fs');
function loadData() {
const users = fs.readFileSync('users.json');
const posts = fs.readFileSync('posts.json');
return { users, posts };
}
// Rewrite to non-blocking
2. Build async API endpoint:
// Create Express endpoint that:
// - Read user file from disk
// - Query database for user data
// - Return combined response
// Use async/await
3. Parallel operations:
// Write function that read 5 files
// Do NOT read one by one
// Read all at same time using Promise.all
4. Error handling:
// Add proper try/catch
// Handle file read errors
// Handle database errors
// Return error responses
5. Performance test:
// Create two versions:
// - Blocking (sequential read)
// - Non-blocking (parallel read)
// Test speed difference
// Measure time
Quick Recap
Blocking code = wait for operation. Nothing else happen.
Non-blocking code = start operation, move on. Callback when done.
Blocking freeze server. All users wait.
Non-blocking allow server handle many users.
Sync methods =
readFileSync,querySync. Avoid in production.Async methods =
readFile,query. Use these.Callbacks = function run later. Basic async pattern.
Promises = cleaner callbacks. Chain operations.
Async/await = easiest pattern. Look like normal code.
asyncfunction = return Promise automatically.awaitkeyword = pause until Promise done.try/catch= handle errors in async code.Promise.all= run multiple async at once (parallel).Sequential = one after another (slow).
Parallel = all together (fast).
3 users, 5 seconds each = blocking need 15 seconds total.
3 users, 5 seconds each = non-blocking do 5 seconds total.
Most operations are I/O: files, database, network. Use async.
Block only on startup, not during request handling.
Non-blocking code = fast servers. Fast servers = happy users.
If you enjoyed this article, check out my other blogs on this profile. Connect with me: LinkedIn | GitHub | X (Twitter)



