Skip to main content

Command Palette

Search for a command to run...

Blocking vs Non-Blocking Code in Node.js

Updated
13 min read
Blocking vs Non-Blocking Code in Node.js
H
CS undergrad | Tech enthusiast | Focusing on Web Dev • DSA • ML | Building skills for real-world impact

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:

  1. Server startup - Load config before start
  2. CLI tools - Command line tool need blocking read
  3. 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.

  • async function = return Promise automatically.

  • await keyword = 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)