# Blocking vs Non-Blocking Code in Node.js: Why It Matters for Your Server

## **Introduction: A Tale of Two Servers**

Picture two restaurants side by side on the same street. Both serve the same food. Both have the same number of tables. Both receive the same number of customers on a busy Friday evening.

In the first restaurant, there is one waiter. When a customer orders a steak, the waiter walks to the kitchen, stands at the counter, and waits. He does not take any other orders. He does not bring water to other tables. He does not check on anyone. He simply stands at the kitchen counter until the steak is ready, which takes twenty minutes. Only then does he return to his section, deliver the food, and move on to the next table. By the end of the night, this restaurant has served twelve tables.

In the second restaurant, there is also one waiter. When a customer orders a steak, the waiter walks to the kitchen, hands in the order, and immediately walks back out to take the next table's order. When that order is in, she checks on another table. She brings water somewhere else. When the kitchen calls out that the steak is ready, she picks it up and delivers it. By the end of the night, this restaurant has served forty tables.

Same waiter. Same kitchen. Same food. The difference is entirely in how the waiter handles waiting time.

This is the difference between blocking and non-blocking code in Node.js. Your JavaScript runs on a single thread — one waiter. How that thread handles waiting for slow operations determines everything about how well your server performs under load.

This article will explain what blocking code is, what non-blocking code is, why the distinction matters so profoundly in Node.js, and how to write code that keeps your server's thread moving efficiently.

* * *

## **Part 1: Understanding Blocking Code**

Blocking code is code that stops execution and waits for an operation to complete before moving on to the next line. While the code is waiting, nothing else can happen. The thread is occupied, frozen at that point, unable to do any other work.

### **The Simple Definition**

When you call a blocking function, execution halts at that line. The function does its work. It finishes. Only then does your code continue to the next line. The thread is blocked for the entire duration of the operation.

```javascript
const fs = require('fs');

console.log('Line 1: About to read a file');

// This is BLOCKING
// Execution stops here until the file is completely read
const content = fs.readFileSync('large-file.txt', 'utf8');

// This line does not run until readFileSync is completely done
console.log('Line 2: File has been read');
console.log('Line 3: File has', content.length, 'characters');
```

The output of this code is always:

```plaintext
Line 1: About to read a file
Line 2: File has been read
Line 3: File has 52418 characters
```

That might seem normal. Sequential execution is how most people learn to think about code. But the critical detail is what is happening during the time between Line 1 and Line 2. If reading the file takes 50 milliseconds, the thread is completely frozen for those 50 milliseconds.

In a web server, frozen means something very specific and very bad. It means every other request that arrives during those 50 milliseconds has to wait. Every user of your application experiences a delay. Not because their request is being processed slowly, but because the thread is stuck waiting for a file that has nothing to do with their request.

### **What Blocking Looks Like at the Thread Level**

To see why blocking is so damaging in Node.js specifically, you need to understand what is happening at the level of the single JavaScript thread.

```plaintext
TIME →

Thread: [Start request A] [readFileSync...waiting...waiting...waiting...done] [Send response A]

                          ↑ During this entire time ↑
                          Requests B, C, D, E arrive and queue up
                          None of them can be processed
                          All users experience delays
```

The thread is like a single-lane road. When there is a blockage, everything behind it backs up. There is no way around it.

### **Common Sources of Blocking in Node.js**

Node.js provides synchronous (blocking) versions of many operations. They are identifiable by the `Sync` suffix:

```javascript
// Blocking file system operations
fs.readFileSync()       // Reads entire file, blocks until done
fs.writeFileSync()      // Writes file, blocks until done
fs.appendFileSync()     // Appends to file, blocks until done
fs.existsSync()         // Checks if file exists, blocks until done
fs.mkdirSync()          // Creates directory, blocks until done
fs.readdirSync()        // Reads directory contents, blocks until done
fs.statSync()           // Gets file info, blocks until done
fs.unlinkSync()         // Deletes file, blocks until done

// Blocking network operations (these exist in some libraries)
// Blocking cryptography
crypto.pbkdf2Sync()     // Hashes passwords, can take hundreds of ms

// Blocking parsing
JSON.parse()            // On very large JSON strings, blocks noticeably
```

The synchronous versions are not without legitimate use. During application startup — reading a configuration file before your server begins accepting connections, for example — synchronous operations are acceptable because no requests are being served yet. The thread being blocked during startup has no cost. But in request handlers, in routes, in any code that runs while your server is actively serving users, synchronous operations are a performance problem.

### **A Demonstration of Blocking Impact**

Here is a server that demonstrates blocking damage in a way you can measure:

```javascript
const http = require('http');
const fs = require('fs');
const path = require('path');

// Create a large file to read (run this once to set up)
// fs.writeFileSync('large-file.txt', 'x'.repeat(10000000));

const server = http.createServer(function(req, res) {

    if (req.url === '/blocking') {
        // BLOCKING: Reads the file synchronously
        // Thread freezes for the duration of the read
        const content = fs.readFileSync('large-file.txt', 'utf8');

        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('Read ' + content.length + ' characters (blocking)');

    } else if (req.url === '/fast') {
        // Fast route with no I/O
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('Fast response, no I/O');

    } else {
        res.writeHead(404);
        res.end('Not found');
    }
});

server.listen(3000, function() {
    console.log('Server running on port 3000');
});
```

If you run this server and send a request to `/blocking`, the thread freezes while reading the file. If you simultaneously send ten requests to `/fast` — which should be nearly instantaneous — every single one of them will wait until the `/blocking` request finishes. Ten users asking for something that should take one millisecond each sit and wait because one unrelated request is blocking the thread.

This is the core problem. In a multi-threaded server, blocking one thread does not affect other threads. In Node.js, blocking the single JavaScript thread affects every user of your application simultaneously.

* * *

## **Part 2: Understanding Non-Blocking Code**

Non-blocking code starts an operation and immediately moves on. It does not wait for the operation to complete. Instead, it provides a way to handle the result when the operation eventually finishes — through a callback function, a Promise, or the async/await syntax that wraps Promises.

### **The Core Idea**

When you call a non-blocking function, execution does not stop. The operation is initiated, a handler is registered to deal with the result, and your code continues to the next line immediately. The operation completes at some point in the future. When it does, the handler runs.

```javascript
const fs = require('fs');

console.log('Line 1: About to read a file');

// This is NON-BLOCKING
// Execution continues immediately after registering the callback
fs.readFile('large-file.txt', 'utf8', function(err, content) {
    // This runs LATER, when the file is done being read
    console.log('Line 3: File has been read');
    console.log('Line 4: File has', content.length, 'characters');
});

// This runs IMMEDIATELY, before the file is done being read
console.log('Line 2: Continuing to do other things');
```

The output of this code is:

```plaintext
Line 1: About to read a file
Line 2: Continuing to do other things
Line 3: File has been read
Line 4: File has 52418 characters
```

Line 2 runs before Lines 3 and 4, even though it appears after the `readFile` call in the source code. This is the nature of non-blocking code: the future result is handled later, asynchronously, while the thread continues immediately.

### **What Non-Blocking Looks Like at the Thread Level**

```plaintext
TIME →

Thread: [Start request A] [start file read, register callback] [immediately free]
                                                                [Handle request B]
                                                                [Handle request C]
                                     File read completes → [Run callback] [Send response A]
                                                                [Handle request D]

The thread is only occupied when actively doing work
Waiting happens in the background, invisible to the thread
Multiple requests progress simultaneously
```

The thread is never frozen. It handles work as it becomes available, responds to completed operations through callbacks, and keeps moving. This is why Node.js can handle thousands of concurrent connections with a single thread.

### **How the Background Work Actually Happens**

When your JavaScript code calls a non-blocking function like `fs.readFile()`, the actual file reading does not happen in your JavaScript thread. It is handed off to a library called libuv, which is the underlying C library that powers Node.js's async capabilities.

libuv has its own thread pool. The file reading happens on one of those threads, completely separate from your JavaScript thread. When the read completes, libuv places the callback in a queue. The event loop picks it up and executes it on your JavaScript thread when the thread is free.

```plaintext
YOUR JAVASCRIPT THREAD:
fs.readFile() call → hands work to libuv → immediately continues

LIBUV THREAD POOL (separate, invisible to your JS):
[Actually reading the file from disk...............done]
                                                     ↓
                                          Signals completion

EVENT LOOP:
Detects completion → places callback in queue → dispatches to JS thread when free

YOUR JAVASCRIPT THREAD (later):
Callback executes with the file content
```

Your JavaScript code never blocks. The actual I/O work happens somewhere else. This architecture is what makes non-blocking I/O possible in a single-threaded environment.

* * *

## **Part 3: Three Ways to Write Non-Blocking Code**

Node.js has evolved through three generations of async syntax. Each generation is built on the same underlying mechanism, but the way you write the code differs. Understanding all three is important because you will encounter them in different codebases.

### **Generation 1: Callbacks**

The original way to handle async results in Node.js is through callbacks — functions you pass to an async operation to be called when it completes.

The Node.js convention for callbacks is that the first parameter is always an error (or null if no error occurred), and subsequent parameters are the results.

```javascript
const fs = require('fs');

function readUserFile(userId, callback) {
    const filename = 'user-' + userId + '.json';

    fs.readFile(filename, 'utf8', function(err, data) {

        // First, check if an error occurred
        if (err) {
            // Pass the error to the callback
            return callback(err, null);
        }

        // Parse the JSON data
        let user;
        try {
            user = JSON.parse(data);
        } catch (parseError) {
            return callback(new Error('Invalid JSON in user file'), null);
        }

        // Pass null as the error (no error) and the result
        callback(null, user);
    });
}

// Using the function
readUserFile(42, function(err, user) {
    if (err) {
        console.error('Failed to read user:', err.message);
        return;
    }
    console.log('User name:', user.name);
});
```

Callbacks work, but they have a significant readability problem when you need to chain multiple async operations. The second operation needs the result of the first, the third needs the result of the second, and so on. The nesting grows quickly:

```javascript
// Callback nesting — sometimes called "callback hell"
fs.readFile('config.json', 'utf8', function(err, configData) {
    if (err) return handleError(err);

    const config = JSON.parse(configData);

    database.connect(config.dbUrl, function(err, connection) {
        if (err) return handleError(err);

        connection.query('SELECT * FROM users', function(err, users) {
            if (err) return handleError(err);

            fs.writeFile('output.json', JSON.stringify(users), function(err) {
                if (err) return handleError(err);

                console.log('Done');
                // Four levels deep, and we have only done four operations
            });
        });
    });
});
```

Each operation nests inside the previous one's callback. With many sequential operations, this becomes difficult to read and maintain.

### **Generation 2: Promises**

Promises were introduced to address callback nesting. A Promise is an object that represents a value that will be available in the future. It can be in one of three states: pending (the operation is not yet complete), fulfilled (the operation completed successfully), or rejected (the operation failed).

```javascript
const fs = require('fs').promises;  // Promise-based version of fs

function readUserFile(userId) {
    const filename = 'user-' + userId + '.json';

    // fs.promises.readFile returns a Promise
    return fs.readFile(filename, 'utf8')
        .then(function(data) {
            return JSON.parse(data);
        });
}

// Using the function
readUserFile(42)
    .then(function(user) {
        console.log('User name:', user.name);
    })
    .catch(function(err) {
        console.error('Failed to read user:', err.message);
    });
```

Promises can be chained, which solves the nesting problem:

```javascript
const fs = require('fs').promises;
const database = require('./database');  // Assuming this returns Promises

// Chained Promises — flat instead of nested
fs.readFile('config.json', 'utf8')
    .then(function(configData) {
        return JSON.parse(configData);
    })
    .then(function(config) {
        return database.connect(config.dbUrl);
    })
    .then(function(connection) {
        return connection.query('SELECT * FROM users');
    })
    .then(function(users) {
        return fs.writeFile('output.json', JSON.stringify(users));
    })
    .then(function() {
        console.log('Done');
    })
    .catch(function(err) {
        // Any error from any step comes here
        console.error('Something went wrong:', err.message);
    });
```

Each `.then()` receives the result of the previous step. The chain stays flat instead of nesting deeper with each step.

### **Generation 3: Async/Await**

Async/await was added to JavaScript to make asynchronous code look and read like synchronous code, while still being non-blocking under the hood.

The `async` keyword before a function declaration makes that function return a Promise automatically. The `await` keyword inside an async function pauses execution of that function until a Promise resolves, then gives you the resolved value. Crucially, while an `await` is waiting, the JavaScript thread is not blocked. It is free to handle other work.

```javascript
const fs = require('fs').promises;

async function readUserFile(userId) {
    const filename = 'user-' + userId + '.json';

    const data = await fs.readFile(filename, 'utf8');
    const user = JSON.parse(data);
    return user;
}

// Using the function
async function main() {
    try {
        const user = await readUserFile(42);
        console.log('User name:', user.name);
    } catch (err) {
        console.error('Failed to read user:', err.message);
    }
}

main();
```

The same sequential async operations from the callback example, written with async/await:

```javascript
const fs = require('fs').promises;

async function processData() {
    try {
        const configData = await fs.readFile('config.json', 'utf8');
        const config = JSON.parse(configData);

        const connection = await database.connect(config.dbUrl);
        const users = await connection.query('SELECT * FROM users');

        await fs.writeFile('output.json', JSON.stringify(users));

        console.log('Done');
    } catch (err) {
        console.error('Something went wrong:', err.message);
    }
}
```

This reads almost like synchronous code. Sequential lines, top to bottom, using familiar try/catch for error handling. But it is completely non-blocking. Each `await` yields the thread while waiting, allowing other requests to be processed.

Async/await is the most commonly used syntax for modern Node.js code. The examples in the rest of this article will use async/await, though the same concepts apply to callbacks and Promises.

* * *

## **Part 4: Real-World Examples**

Understanding blocking and non-blocking in the abstract is one thing. Seeing the difference in realistic server code makes it concrete. Here are the three most common sources of blocking in real applications and how to handle them non-blockingly.

### **File System Operations**

File reading and writing are the most commonly illustrated examples of blocking vs non-blocking, and with good reason. Files can be large, disk access can be slow (especially on shared cloud storage), and many applications do significant file I/O.

**The Blocking Version:**

```javascript
const express = require('express');
const fs = require('fs');
const app = express();

app.get('/report', function(req, res) {
    try {
        // BLOCKING: Thread frozen until file is completely read
        // If this takes 100ms and 50 requests arrive, they all wait
        const reportData = fs.readFileSync('monthly-report.json', 'utf8');
        const report = JSON.parse(reportData);

        res.json({
            success: true,
            data: report
        });
    } catch (err) {
        res.status(500).json({ error: 'Failed to load report' });
    }
});

app.listen(3000);
```

If `monthly-report.json` is a large file and takes 200ms to read, every request that arrives during those 200ms is queued. With ten simultaneous users hitting this endpoint, the tenth user waits for the previous nine reads to complete — potentially waiting 1800ms for what should have been a 200ms operation.

**The Non-Blocking Version:**

```javascript
const express = require('express');
const fs = require('fs').promises;
const app = express();

app.get('/report', async function(req, res) {
    try {
        // NON-BLOCKING: Thread free while file is being read
        // Other requests can be handled during the read
        const reportData = await fs.readFile('monthly-report.json', 'utf8');
        const report = JSON.parse(reportData);

        res.json({
            success: true,
            data: report
        });
    } catch (err) {
        res.status(500).json({ error: 'Failed to load report' });
    }
});

app.listen(3000);
```

With `await`, the thread yields during the file read. Other requests can be processed while the file loads. Ten simultaneous users each get their file read happening concurrently. The tenth user waits approximately 200ms, not 1800ms.

**Handling Multiple Files:**

Sometimes you need to read several files. You can read them sequentially (each awaits before starting the next) or concurrently (all start at once with Promise.all).

```javascript
app.get('/dashboard', async function(req, res) {
    try {

        // SEQUENTIAL: Total time = sum of all read times
        // Each read waits for the previous to finish
        const userData = await fs.readFile('users.json', 'utf8');
        const ordersData = await fs.readFile('orders.json', 'utf8');
        const productsData = await fs.readFile('products.json', 'utf8');

        // If each takes 50ms, total time is ~150ms

    } catch (err) {
        res.status(500).json({ error: 'Failed to load data' });
    }
});
```

```javascript
app.get('/dashboard', async function(req, res) {
    try {

        // CONCURRENT: Total time = maximum single read time
        // All reads start simultaneously
        const [userData, ordersData, productsData] = await Promise.all([
            fs.readFile('users.json', 'utf8'),
            fs.readFile('orders.json', 'utf8'),
            fs.readFile('products.json', 'utf8')
        ]);

        // If each takes 50ms, total time is ~50ms (they run together)

        const users = JSON.parse(userData);
        const orders = JSON.parse(ordersData);
        const products = JSON.parse(productsData);

        res.json({
            success: true,
            data: { users, orders, products }
        });
    } catch (err) {
        res.status(500).json({ error: 'Failed to load data' });
    }
});
```

`Promise.all` starts all operations simultaneously and waits for all of them to complete. The total wait time is approximately the duration of the longest individual operation, not the sum of all operations.

**Reading a File in Chunks With Streams:**

For very large files, even non-blocking reads load the entire file into memory before processing it. Streams process the file in chunks as data arrives, keeping memory usage low.

```javascript
const express = require('express');
const fs = require('fs');
const app = express();

app.get('/download-large-file', function(req, res) {
    const filename = 'very-large-dataset.csv';

    // Check if file exists first
    fs.access(filename, fs.constants.F_OK, function(err) {
        if (err) {
            return res.status(404).json({ error: 'File not found' });
        }

        res.setHeader('Content-Type', 'text/csv');
        res.setHeader('Content-Disposition', 'attachment; filename="data.csv"');

        // Stream the file: data flows chunk by chunk
        // Never loads the entire file into memory
        const fileStream = fs.createReadStream(filename);

        fileStream.on('error', function(streamErr) {
            res.status(500).end('Error reading file');
        });

        // Pipe directly to the response
        fileStream.pipe(res);
    });
});
```

With streaming, a 5GB file can be sent to a client without ever holding 5GB in memory. Data flows through as it is read, keeping both memory usage and response time low.

### **Database Operations**

Database queries are among the most common operations in web servers and almost always the dominant source of latency. A query that joins several tables, searches a large dataset, or involves a slow connection might take anywhere from a few milliseconds to several seconds.

**The Blocking Pattern (Conceptual):**

Some older or improperly configured database libraries offer synchronous query methods. Using them freezes the thread for the entire query duration.

```javascript
// CONCEPTUAL ONLY - avoid synchronous DB calls
app.get('/users/:id', function(req, res) {
    const userId = parseInt(req.params.id);

    // Pretend this is a blocking database call (do not do this)
    const user = database.querySync('SELECT * FROM users WHERE id = ?', [userId]);

    if (!user) {
        return res.status(404).json({ error: 'User not found' });
    }

    res.json({ data: user });
});
```

If this query takes 80ms, the thread is blocked for 80ms. A server handling 500 requests per second would queue 40 requests behind each blocking database call.

**The Non-Blocking Pattern with Async/Await:**

Modern database drivers for Node.js return Promises, making them naturally compatible with async/await.

```javascript
const express = require('express');
const { Pool } = require('pg');  // PostgreSQL client
const app = express();

const pool = new Pool({
    host: 'localhost',
    database: 'myapp',
    user: 'dbuser',
    password: 'password',
    port: 5432
});

// Get a single user
app.get('/users/:id', async function(req, res) {
    const userId = parseInt(req.params.id, 10);

    if (isNaN(userId) || userId <= 0) {
        return res.status(400).json({ error: 'Invalid user ID' });
    }

    try {
        // NON-BLOCKING: Thread yields during query execution
        // Other requests can run while the database works
        const result = await pool.query(
            'SELECT id, name, email, role, created_at FROM users WHERE id = $1',
            [userId]
        );

        if (result.rows.length === 0) {
            return res.status(404).json({ error: 'User not found' });
        }

        res.json({
            success: true,
            data: result.rows[0]
        });

    } catch (err) {
        console.error('Database error:', err.message);
        res.status(500).json({ error: 'Database operation failed' });
    }
});

// Get users with their order counts
app.get('/users', async function(req, res) {
    try {
        const result = await pool.query(`
            SELECT
                u.id,
                u.name,
                u.email,
                u.role,
                COUNT(o.id) AS order_count,
                MAX(o.created_at) AS last_order_date
            FROM users u
            LEFT JOIN orders o ON o.user_id = u.id
            GROUP BY u.id
            ORDER BY u.name
            LIMIT 50
        `);

        res.json({
            success: true,
            count: result.rows.length,
            data: result.rows
        });

    } catch (err) {
        console.error('Database error:', err.message);
        res.status(500).json({ error: 'Database operation failed' });
    }
});

// Dashboard endpoint requiring multiple queries
app.get('/dashboard/:userId', async function(req, res) {
    const userId = parseInt(req.params.userId, 10);

    try {
        // Run independent queries concurrently instead of sequentially
        const [userResult, ordersResult, notificationsResult] = await Promise.all([

            pool.query(
                'SELECT id, name, email FROM users WHERE id = $1',
                [userId]
            ),

            pool.query(
                'SELECT id, total, status, created_at FROM orders WHERE user_id = $1 ORDER BY created_at DESC LIMIT 5',
                [userId]
            ),

            pool.query(
                'SELECT COUNT(*) AS count FROM notifications WHERE user_id = $1 AND read = false',
                [userId]
            )
        ]);

        if (userResult.rows.length === 0) {
            return res.status(404).json({ error: 'User not found' });
        }

        res.json({
            success: true,
            data: {
                user: userResult.rows[0],
                recentOrders: ordersResult.rows,
                unreadNotifications: parseInt(notificationsResult.rows[0].count)
            }
        });

    } catch (err) {
        console.error('Database error:', err.message);
        res.status(500).json({ error: 'Database operation failed' });
    }
});

app.listen(3000);
```

Notice the dashboard endpoint uses `Promise.all` to run three independent database queries simultaneously. Instead of three sequential queries taking 30ms + 25ms + 20ms = 75ms total, all three run concurrently and the total time is approximately 30ms (the slowest query).

**Connection Pooling:**

Database connections themselves take time to establish. Creating a new connection for every request is inefficient. Connection pooling maintains a set of already-established connections that requests can reuse.

```javascript
// Pool configuration
const pool = new Pool({
    host: process.env.DB_HOST,
    database: process.env.DB_NAME,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    port: 5432,

    // Pool settings
    max: 20,                 // Maximum number of connections in pool
    min: 2,                  // Minimum connections to keep ready
    idleTimeoutMillis: 30000,  // Close idle connections after 30 seconds
    connectionTimeoutMillis: 2000  // Error if cannot get connection in 2 seconds
});

// The pool is non-blocking: if all connections are busy,
// your request waits in a queue until one is available
// This wait is non-blocking — the thread is free during the wait
async function queryDatabase(sql, params) {
    const client = await pool.connect();  // Non-blocking wait for connection
    try {
        const result = await client.query(sql, params);
        return result.rows;
    } finally {
        client.release();  // Always return connection to pool
    }
}
```

### **External API Calls**

Web applications frequently call external services: payment processors, email providers, mapping services, weather APIs, authentication providers. Each call involves a network round trip that can take anywhere from 50ms to several seconds depending on the service, the network, and the geographic distance.

```javascript
const express = require('express');
const https = require('https');
const app = express();

// Helper: wrap https.get in a Promise for use with async/await
function fetchJson(url) {
    return new Promise(function(resolve, reject) {
        https.get(url, function(response) {
            let data = '';

            response.on('data', function(chunk) {
                data += chunk;
            });

            response.on('end', function() {
                try {
                    resolve(JSON.parse(data));
                } catch (err) {
                    reject(new Error('Invalid JSON response'));
                }
            });

        }).on('error', function(err) {
            reject(err);
        });
    });
}

// Enrich user data with external information
app.get('/users/:id/profile', async function(req, res) {
    const userId = req.params.id;

    try {
        // These external calls can run concurrently
        const [githubProfile, twitterProfile] = await Promise.all([
            fetchJson('https://api.github.com/users/' + userId).catch(function() {
                return null;  // Return null if the call fails
            }),
            fetchJson('https://api.twitter.com/users/' + userId).catch(function() {
                return null;
            })
        ]);

        res.json({
            success: true,
            data: {
                userId: userId,
                github: githubProfile,
                twitter: twitterProfile
            }
        });

    } catch (err) {
        res.status(500).json({ error: 'Failed to fetch profile data' });
    }
});

// Place an order: validates payment AND notifies warehouse simultaneously
app.post('/orders', express.json(), async function(req, res) {
    const { userId, items, paymentMethod } = req.body;

    try {
        // First, validate the payment (must complete before creating order)
        const paymentResult = await fetchJson(
            'https://api.payment-processor.com/validate/' + paymentMethod
        );

        if (!paymentResult.valid) {
            return res.status(402).json({ error: 'Payment method is not valid' });
        }

        // Payment valid, create the order (from local DB)
        const order = { id: Date.now(), userId, items, status: 'created' };

        // Notify warehouse and send confirmation email concurrently
        // Neither depends on the other, so run them simultaneously
        await Promise.all([

            fetchJson('https://api.warehouse.com/new-order/' + order.id).catch(
                function(err) {
                    console.error('Warehouse notification failed:', err.message);
                    // Non-critical: continue even if this fails
                }
            ),

            fetchJson('https://api.email-service.com/send/order-confirmation/' + order.id).catch(
                function(err) {
                    console.error('Email failed:', err.message);
                    // Non-critical: continue even if this fails
                }
            )
        ]);

        res.status(201).json({
            success: true,
            message: 'Order placed successfully',
            data: order
        });

    } catch (err) {
        console.error('Order creation error:', err.message);
        res.status(500).json({ error: 'Failed to place order' });
    }
});

app.listen(3000);
```

The key patterns here are the same as with file and database operations:

Operations that do not depend on each other run concurrently with `Promise.all`. Operations that depend on each other run sequentially with individual `await` calls. Errors from non-critical operations are caught and logged without stopping the main flow.

* * *

## **Part 5: Async Patterns for Common Situations**

Beyond individual async operations, certain patterns come up repeatedly when writing non-blocking code.

### **Sequential Operations That Depend on Each Other**

When each operation needs the result of the previous one, they must run sequentially. Async/await makes this natural:

```javascript
async function createOrderWithInventoryCheck(orderData) {

    // Step 1: Get the product (needed for step 2)
    const product = await database.findProduct(orderData.productId);
    if (!product) {
        throw new Error('Product not found');
    }

    // Step 2: Check inventory (needs product, needed for step 3)
    const inventory = await database.checkInventory(product.id, orderData.quantity);
    if (inventory.available < orderData.quantity) {
        throw new Error('Insufficient inventory');
    }

    // Step 3: Create the order (needs product and inventory confirmation)
    const order = await database.createOrder({
        productId: product.id,
        quantity: orderData.quantity,
        price: product.price * orderData.quantity
    });

    // Step 4: Update inventory (needs order)
    await database.decrementInventory(product.id, orderData.quantity);

    return order;
}
```

Each step awaits before the next begins, because each depends on the previous result. The thread is free during each wait, but the operations must occur in this order.

### **Independent Operations That Can Run Concurrently**

When operations do not depend on each other, run them simultaneously:

```javascript
async function getUserDashboardData(userId) {

    // These three operations are completely independent
    // No reason to run them sequentially
    const [
        userProfile,
        recentActivity,
        recommendations
    ] = await Promise.all([
        database.getUserProfile(userId),
        database.getRecentActivity(userId),
        recommendationService.getRecommendations(userId)
    ]);

    return {
        profile: userProfile,
        activity: recentActivity,
        recommendations: recommendations
    };
}
```

`Promise.all` starts all three simultaneously. Total time is approximately the longest individual operation, not the sum of all three.

### **Processing a Collection of Items**

When you have an array of items and need to perform async operations on each one, the approach depends on whether order matters and how many items there are.

**Process all items concurrently (fast, use when safe):**

```javascript
async function sendEmailsToAllUsers(users) {
    // Start all emails at the same time
    const emailPromises = users.map(function(user) {
        return emailService.send({
            to: user.email,
            subject: 'Monthly newsletter',
            body: generateNewsletter(user)
        });
    });

    const results = await Promise.all(emailPromises);
    return results;
}
```

This sends all emails simultaneously. Fast, but if you have 10,000 users, you are making 10,000 simultaneous requests to the email service, which may overwhelm it.

**Process items in batches (balanced approach):**

```javascript
async function sendEmailsInBatches(users, batchSize) {
    const results = [];

    for (let i = 0; i < users.length; i += batchSize) {
        // Take a batch of users
        const batch = users.slice(i, i + batchSize);

        // Process this batch concurrently
        const batchResults = await Promise.all(
            batch.map(function(user) {
                return emailService.send({
                    to: user.email,
                    subject: 'Monthly newsletter',
                    body: generateNewsletter(user)
                });
            })
        );

        results.push(...batchResults);

        // Optional: small delay between batches to avoid overwhelming the service
        if (i + batchSize < users.length) {
            await new Promise(function(resolve) {
                setTimeout(resolve, 100);  // 100ms pause between batches
            });
        }
    }

    return results;
}

// Usage: process 50 users at a time
sendEmailsInBatches(allUsers, 50);
```

Batching gives you concurrency within each batch without overwhelming external services.

**Process items strictly sequentially (when order or rate limits matter):**

```javascript
async function processOrdersSequentially(orders) {
    const results = [];

    for (const order of orders) {
        // Process one at a time
        // Only starts the next when the current is complete
        const result = await processOrder(order);
        results.push(result);
    }

    return results;
}
```

Sequential processing is slower but necessary when operations must occur in order or when you are strictly rate-limited to one operation at a time.

### **Handling Errors in Async Code**

Error handling in async/await uses try/catch, which behaves consistently and intuitively:

```javascript
async function robustDataFetch(userId) {

    // Wrap everything in try/catch to handle any async error
    try {
        const user = await database.findUser(userId);

        if (!user) {
            // Throw specific errors for different conditions
            throw new Error('User not found');
        }

        const data = await externalService.fetchUserData(user.externalId);
        return { user, data };

    } catch (err) {
        // Log the error for debugging
        console.error('Data fetch failed for user', userId, ':', err.message);

        // Re-throw so the caller can handle it
        throw err;
    }
}

// Handling errors from Promise.all
async function fetchMultipleWithFallbacks(userId) {
    try {

        // Promise.allSettled continues even if some promises fail
        // Unlike Promise.all which stops at the first failure
        const results = await Promise.allSettled([
            database.findUser(userId),
            externalService.fetchProfile(userId),
            cacheService.getUserCache(userId)
        ]);

        const user = results[0].status === 'fulfilled'
            ? results[0].value
            : null;

        const profile = results[1].status === 'fulfilled'
            ? results[1].value
            : null;

        const cached = results[2].status === 'fulfilled'
            ? results[2].value
            : null;

        return { user, profile, cached };

    } catch (err) {
        console.error('Unexpected error:', err.message);
        throw err;
    }
}
```

`Promise.allSettled` is different from `Promise.all`. While `Promise.all` fails fast — if any Promise rejects, the whole thing rejects — `Promise.allSettled` waits for all Promises to complete regardless of success or failure, then gives you the status of each one. Use it when you want to attempt all operations and handle individual failures separately.

* * *

## **Part 6: The One Thing You Must Never Do — CPU-Heavy Work on the Main Thread**

Everything discussed so far about non-blocking code assumes that the slow part is I/O — waiting for files, databases, or networks. Node.js handles I/O waiting non-blockingly because the waiting happens in libuv's background threads, not on your JavaScript thread.

But there is a category of work that cannot be offloaded this way: CPU-intensive computation. Tasks that require the processor to actively compute for extended periods — not waiting for external resources, but genuinely working.

```javascript
// This BLOCKS the event loop, regardless of whether it's "async"
function findLargestPrime(limit) {
    let largest = 2;
    for (let num = 3; num <= limit; num++) {
        let isPrime = true;
        for (let divisor = 2; divisor <= Math.sqrt(num); divisor++) {
            if (num % divisor === 0) {
                isPrime = false;
                break;
            }
        }
        if (isPrime) largest = num;
    }
    return largest;
}

// This blocks the entire server for the duration of the calculation
app.get('/largest-prime', function(req, res) {
    const result = findLargestPrime(10000000);  // Takes several seconds
    res.json({ result });
});
```

The `findLargestPrime` function keeps the processor actively computing. It is not waiting for I/O. Making it `async` or wrapping it in a Promise does not help — the JavaScript thread is genuinely occupied.

If this endpoint is called, no other request can be handled for the several seconds it takes to compute. The event loop is blocked. The single JavaScript thread is grinding through the calculation.

For CPU-intensive work in Node.js, the solutions involve moving the work off the main thread:

**Worker Threads:** Node.js provides a `worker_threads` module that lets you run JavaScript on separate threads. Use a worker thread for CPU-intensive computation, communicate results back through messages.

```javascript
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
    // This code runs in the main thread

    app.get('/largest-prime', function(req, res) {
        // Offload to a worker thread
        const worker = new Worker(__filename, {
            workerData: { limit: 10000000 }
        });

        worker.on('message', function(result) {
            res.json({ result });
        });

        worker.on('error', function(err) {
            res.status(500).json({ error: err.message });
        });
    });

} else {
    // This code runs in the worker thread
    const result = findLargestPrime(workerData.limit);
    parentPort.postMessage(result);
}
```

The main thread is free while the worker thread does the calculation. Other requests can be handled during that time.

The practical guidance: Node.js is optimized for I/O-bound work. For CPU-bound work, offload to worker threads, child processes, or purpose-built services written in languages designed for computation.

* * *

## **Part 7: Identifying and Fixing Blocking Code in Real Applications**

Knowing the principles is one thing. Recognizing blocking code in a real codebase is another. Here is a practical guide to finding and fixing blocking patterns.

### **Red Flags: Patterns to Look For**

```javascript
// RED FLAG 1: Any method ending in Sync inside a request handler
fs.readFileSync()           // Should be: await fs.promises.readFile()
fs.writeFileSync()          // Should be: await fs.promises.writeFile()
fs.existsSync()             // Should be: await fs.promises.access()
child_process.execSync()    // Should be: child_process.exec() with callback/Promise
crypto.pbkdf2Sync()         // Should be: await crypto.pbkdf2() (promisified)

// RED FLAG 2: Heavy loops or computation in route handlers
app.get('/stats', function(req, res) {
    let total = 0;
    for (let i = 0; i < 100000000; i++) {  // Hundreds of millions of iterations
        total += Math.random();
    }
    res.json({ total });
});

// RED FLAG 3: JSON.parse on very large strings
app.post('/import', function(req, res) {
    const data = JSON.parse(req.body.largeJsonString);  // Can block on large data
    // Process data...
});

// RED FLAG 4: Synchronous password hashing
const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512');
// Should be: const hash = await util.promisify(crypto.pbkdf2)(password, salt, 100000, 64, 'sha512');
```

### **Before and After: A Route Refactor**

Here is a request handler that has multiple blocking issues, followed by a corrected version:

```javascript
// BEFORE: Multiple blocking issues
app.post('/process-upload', function(req, res) {
    const filename = req.body.filename;

    // BLOCKING 1: Synchronous file read
    const rawData = fs.readFileSync('./uploads/' + filename, 'utf8');

    // BLOCKING 2: CPU-heavy parsing and processing
    const lines = rawData.split('\n');
    const processed = [];
    for (let i = 0; i < lines.length; i++) {
        const parts = lines[i].split(',');
        // Heavy processing of each line
        processed.push(transformLine(parts));
    }

    // BLOCKING 3: Synchronous file write
    fs.writeFileSync('./output/' + filename, JSON.stringify(processed));

    // BLOCKING 4: Synchronous password hashing (if needed here)
    const hash = crypto.pbkdf2Sync(filename, 'salt', 100000, 64, 'sha512');

    res.json({ success: true, processedLines: processed.length });
});
```

```javascript
// AFTER: Non-blocking version
const fsPromises = require('fs').promises;
const util = require('util');
const pbkdf2 = util.promisify(require('crypto').pbkdf2);

app.post('/process-upload', async function(req, res) {
    const filename = req.body.filename;

    try {
        // NON-BLOCKING 1: Async file read
        const rawData = await fsPromises.readFile('./uploads/' + filename, 'utf8');

        // Still potentially CPU-heavy, but file read is async
        // For very large files, consider streaming or worker threads
        const lines = rawData.split('\n');
        const processed = lines.map(function(line) {
            const parts = line.split(',');
            return transformLine(parts);
        });

        // NON-BLOCKING 2: Async file write
        await fsPromises.writeFile('./output/' + filename, JSON.stringify(processed));

        // NON-BLOCKING 3: Async password hashing
        const hash = await pbkdf2(filename, 'salt', 100000, 64, 'sha512');

        res.json({ success: true, processedLines: processed.length });

    } catch (err) {
        console.error('Processing error:', err.message);
        res.status(500).json({ error: 'Processing failed' });
    }
});
```

The data processing loop is still synchronous — it cannot be made async because it is pure computation. If this loop is genuinely slow (millions of records), it belongs in a worker thread. But the file I/O and cryptography are now properly non-blocking.

* * *

## **Summary**

Blocking code stops the thread and waits. In a single-threaded environment like Node.js, a blocked thread means a frozen server — all requests queue up and wait, regardless of what they need. The degree of damage scales with how long the block lasts and how many users are simultaneously affected.

Non-blocking code initiates an operation, registers a handler for the result, and immediately continues. The waiting happens in the background, managed by libuv. The JavaScript thread remains free to handle other requests. This is what allows Node.js to handle high concurrency on a single thread.

The three syntaxes for non-blocking code — callbacks, Promises, and async/await — are all built on the same underlying mechanism. Async/await is the most readable and is the standard in modern Node.js code.

File system operations have synchronous versions with the `Sync` suffix and asynchronous versions that accept callbacks or return Promises. Use `fs.promises` with async/await for clean, non-blocking file operations. Database queries are inherently async in modern Node.js drivers — await them, and run independent queries concurrently with `Promise.all`. External API calls involve network latency and should always be non-blocking — concurrent calls with `Promise.all` and `Promise.allSettled` for independent operations.

CPU-intensive computation is the exception that non-blocking I/O does not solve. Heavy calculation genuinely occupies the processor and blocks the event loop regardless of async syntax. For computation-heavy work, use worker threads or offload to separate services.

The practical rules are simple: never use `Sync` methods in request handlers, await all async operations, run independent operations concurrently with `Promise.all`, and be aware of the CPU-bound operations that require worker threads. Following these rules keeps your Node.js server's thread moving freely and your application responsive under load.
