Skip to main content

Command Palette

Search for a command to run...

Blocking vs Non-Blocking Code in Node.js

Updated
8 min readView as Markdown

When working with Node.js, one important concept to understand is blocking vs non-blocking code.

The difference is simple:

Blocking code makes the program wait. Non-blocking code allows the program to continue while waiting for an operation to finish.

This becomes especially important when building servers because a server may need to handle many requests at the same time.


What Does Blocking Code Mean?

Blocking code is code that stops the execution of the program until a particular operation is completed.

For example, suppose we want to read a large file synchronously:

const fs = require("fs");

const data = fs.readFileSync("large-file.txt", "utf8");

console.log(data);

console.log("This runs after the file is completely read.");

Here, readFileSync() is a synchronous operation.

Node.js waits for the file to be completely read before moving to the next line.

Blocking Execution

We can visualize it like this:

Main Thread
    │
    ▼
Start File Read
    │
    ▼
┌──────────────────────┐
│   WAIT FOR FILE      │
│      TO FINISH       │
└──────────────────────┘
    │
    ▼
File Read Completed
    │
    ▼
Continue Execution

If reading the file takes 5 seconds, the JavaScript execution is effectively stuck waiting for those 5 seconds.


Why Can Blocking Code Be a Problem?

Imagine that our Node.js server receives multiple requests.

Request 1 ──► Blocking Operation ──► 5 seconds
                                      │
Request 2 ────────────────────────────┘
                                      │
Request 3 ────────────────────────────┘
                                      │
Request 4 ────────────────────────────┘

If the JavaScript thread is blocked by a long-running operation, other work that needs that thread cannot be processed normally.

This can make the server feel slow, especially when many users are sending requests.

For example:

const fs = require("fs");

const data = fs.readFileSync("large-file.txt", "utf8");

console.log("File read completed");

The important point is:

Node.js can execute blocking code. Blocking code simply means that the JavaScript execution has to wait before continuing.


What Does Non-Blocking Code Mean?

Non-blocking code allows the program to continue executing while an asynchronous operation is in progress.

For example:

const fs = require("fs");

fs.readFile("large-file.txt", "utf8", (err, data) => {
    if (err) {
        console.error(err);
        return;
    }

    console.log(data);
});

console.log("This runs while the file is being read.");

Here, readFile() is asynchronous.

Node.js starts the file-reading operation and does not make the JavaScript execution wait for it to finish.

Instead, the program can continue executing.

Once the operation completes, the callback can be processed by the event loop.


Non-Blocking Execution

We can visualize this as:

Main Thread
    │
    ▼
Start File Read
    │
    ├──────────────► File System
    │                    │
    │                    │ Reading file...
    ▼                    │
Continue Execution       │
    │                    │
    ▼                    │
Do Other Work             │
    │                    │
    ▼                    │
Event Loop ◄──────────────┘
    │
    ▼
Execute Callback

The important difference is that the JavaScript execution does not simply sit there waiting for the file operation to finish.


Blocking vs Non-Blocking

Let's compare both approaches.

Blocking Non-Blocking
Execution waits for the operation Execution can continue
Can make the JavaScript thread unavailable Keeps the JavaScript thread available
Usually simpler to understand Requires asynchronous handling
Can hurt server responsiveness for long operations Better suited for I/O-heavy servers
Example: readFileSync() Example: readFile()

The basic idea can be summarized as:

BLOCKING

Task A ────────────────► Complete
                         │
                         ▼
                       Task B


NON-BLOCKING

Task A ───────► Started
                 │
                 │
                 ├──────► Task B
                 │
                 │
                 ▼
              Complete

How Does Node.js Handle Non-Blocking Operations?

Node.js uses an event-driven architecture.

When an asynchronous operation is started, Node.js can rely on the operating system or libuv, depending on the type of operation.

For certain operations, libuv uses a thread pool to perform work away from the main JavaScript execution thread.

When the operation is ready, its callback or continuation can be scheduled to run through Node.js's event-loop mechanism.

A simplified view looks like this:

                 Node.js Application
                         │
                         ▼
                  JavaScript Code
                         │
                         ▼
                    Event Loop
                         │
              ┌──────────┴──────────┐
              │                     │
              ▼                     ▼
        Operating System       libuv mechanisms
              │                     │
              │                Thread Pool
              │              (when needed)
              │                     │
              └──────────┬──────────┘
                         ▼
                  Operation Complete
                         │
                         ▼
                    Event Loop
                         │
                         ▼
                  Callback Executes

Not every asynchronous operation uses a background thread. Some I/O can be handled using operating-system mechanisms, while certain operations use libuv's thread pool.


File Reading Example

Let's look at the difference side by side.

Blocking

const fs = require("fs");

console.log("Start");

const data = fs.readFileSync("file.txt", "utf8");

console.log("File read completed");

console.log("End");

The flow is:

Start
  │
  ▼
Read File
  │
  │ WAIT
  ▼
File Completed
  │
  ▼
File read completed
  │
  ▼
End

Non-Blocking

const fs = require("fs");

console.log("Start");

fs.readFile("file.txt", "utf8", (err, data) => {
    if (err) {
        console.error(err);
        return;
    }

    console.log("File read completed");
});

console.log("End");

The conceptual flow is:

Start
  │
  ▼
Start File Read ─────────► File System
  │                           │
  ▼                           │
End                            │
                              │
                              ▼
                         File Completed
                              │
                              ▼
                         Event Loop
                              │
                              ▼
                    "File read completed"

So the output will generally be:

Start
End
File read completed

because the file-reading operation completes asynchronously.


What About Database and Network Requests?

The same idea applies to many I/O operations.

For example, imagine our server needs to retrieve data from a database.

With a blocking approach:

Server
  │
  ▼
Database Request
  │
  │ WAIT
  ▼
Database Response
  │
  ▼
Continue

With a non-blocking approach:

Server
  │
  ▼
Start Database Request
  │
  ├──────────────► Database
  │                    │
  ▼                    │
Handle Other Work      │
  │                    │
  ▼                    │
Continue                │
                       ▼
                 Database Response
                       │
                       ▼
                  Handle Result

This is one of the reasons asynchronous programming is so important in Node.js.


Real-World Analogy

Imagine a restaurant with a waiter.

Blocking Approach

The waiter takes one customer's order and then stands in the kitchen waiting for the food.

Take Order
    │
    ▼
Wait in Kitchen
    │
    │
    │
Food Ready
    │
    ▼
Serve Customer
    │
    ▼
Take Next Order

During that waiting time, the waiter cannot serve other customers.

Non-Blocking Approach

Instead, the waiter gives the order to the kitchen and immediately serves another customer.

Take Order
    │
    ▼
Send Order to Kitchen
    │
    ├──────────────► Kitchen
    │
    ▼
Serve Another Customer
    │
    ▼
Do Other Work
    │
    ▼
Food Ready
    │
    ▼
Serve Original Customer

This is similar to how non-blocking I/O allows Node.js to keep making progress instead of waiting for every I/O operation to finish.


Why Non-Blocking Code Matters in Node.js

Node.js is commonly used for applications that perform a lot of I/O operations, such as:

  • Reading and writing files

  • Database requests

  • Network requests

  • API calls

  • WebSocket communication

These operations often involve waiting for something outside the JavaScript code.

If we unnecessarily block the JavaScript thread while waiting, the server can become less responsive.

With non-blocking I/O, Node.js can start the operation and continue handling other work.

That's one of the key ideas behind Node.js's ability to handle many concurrent I/O-bound tasks efficiently.


One Important Confusion

It is easy to think:

"Node.js is non-blocking, so Node.js never blocks."

That's not true.

Node.js can absolutely execute blocking code.

For example:

while (true) {
    // CPU keeps running here
}

This kind of CPU-heavy work can block the event loop because the JavaScript thread is busy and cannot move on to other callbacks.

So the better way to think about Node.js is:

Node.js provides powerful asynchronous and non-blocking APIs, but developers can still write blocking code.


Final Takeaway

The core difference is about waiting.

                BLOCKING
                   │
                   ▼
          Start Operation
                   │
                   ▼
                 WAIT
                   │
                   ▼
          Operation Complete
                   │
                   ▼
           Continue Program


              NON-BLOCKING
                   │
                   ▼
          Start Operation
                   │
                   ├────────► Operation continues
                   │
                   ▼
            Continue Program
                   │
                   ▼
          Other work can run
                   │
                   ▼
          Operation Complete
                   │
                   ▼
             Handle Result

In short:

Blocking code waits before continuing. Non-blocking code starts an asynchronous operation and allows the program to continue while waiting for the result.

Understanding this concept is essential before diving deeper into the Node.js Event Loop, because the event loop is one of the mechanisms that allows Node.js to coordinate asynchronous work efficiently.