Part 5 – Mastering Asynchronous Programming

Node.js’s asynchronous nature is its superpower. This guide will help you understand callbacks, promises, and async/await – the three pillars of async programming in Node.js.

1. Understanding the Node.js Event Loop

The event loop is what enables Node.js to handle thousands of concurrent connections with a single thread:

  1. Receives callbacks from completed I/O operations
  2. Executes JavaScript in the main thread
  3. Delegates I/O operations to libuv’s thread pool
  4. Returns results via callbacks when ready

2. Callback Pattern (The Original Approach)

const fs = require('fs');

// Read file with callback
fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) {
        console.error("Error reading file:", err);
        return;
    }
    console.log("File content:", data);
});

Callback Convention: Node.js callbacks always receive error first (err), then results (data).

3. Promise-Based Approach (Modern Alternative)

Using Native Promises

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

fs.readFile('example.txt', 'utf8')
    .then(data => {
        console.log("File content:", data);
    })
    .catch(err => {
        console.error("Error reading file:", err);
    });

Creating Your Own Promise

function readFilePromise(path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, 'utf8', (err, data) => {
            if (err) reject(err);
            else resolve(data);
        });
    });
}

4. Async/Await (Syntactic Sugar for Promises)

async function processFile() {
    try {
        const data = await fs.readFile('example.txt', 'utf8');
        console.log("File content:", data);

        const processed = await processData(data);
        console.log("Processed:", processed);
    } catch (err) {
        console.error("Error:", err);
    }
}

processFile();

Key Rule: await can only be used inside async functions.

5. Comparing All Three Approaches

ApproachProsConsWhen to Use
CallbacksMost basic, works everywhereCallback hell, hard to maintainLegacy code, simple scripts
PromisesChainable, better error handlingStill requires .then()Modern code, when you need control
Async/AwaitCleanest syntax, try/catch worksRequires modern Node.jsMost new code, complex flows

6. Common Async Patterns

Parallel Execution

// Using Promise.all
async function fetchAllData() {
    const [users, products] = await Promise.all([
        fetchUsers(),
        fetchProducts()
    ]);
    return { users, products };
}

Sequential Execution

async function processSteps() {
    const step1 = await doStep1();
    const step2 = await doStep2(step1);
    return await doStep3(step2);
}

7. Error Handling Strategies

Try/Catch with Async/Await

async function safeOperation() {
    try {
        const result = await mightFail();
        return result;
    } catch (err) {
        console.error("Operation failed:", err);
        throw err; // Re-throw if needed
    }
}

Error-First Callbacks

fs.readFile('file.txt', (err, data) => {
    if (err) {
        // Handle error
        return;
    }
    // Process data
});

Catch Handler for Promises

fetchData()
    .then(processData)
    .catch(err => {
        console.error("Error in chain:", err);
    });

8. Real-World Async Example

Fetching data from multiple APIs with error handling:

async function fetchDashboardData(userId) {
    try {
        const [user, orders, notifications] = await Promise.all([
            fetchUser(userId).catch(() => null),
            fetchOrders(userId).catch(() => []),
            fetchNotifications(userId).catch(() => [])
        ]);

        if (!user) {
            throw new Error('User not found');
        }

        return {
            user,
            orders,
            notifications,
            lastUpdated: new Date()
        };
    } catch (err) {
        console.error('Dashboard error:', err);
        throw err;
    }
}

Next: Working with the File System →

Async Best Practices

  • Always handle errors – never leave promises uncaught
  • Use Promise.all() for parallel operations
  • Avoid nesting promises/callbacks too deeply
  • Consider using util.promisify for callback APIs
  • Use async/await for better readability

Leave a Comment

Your email address will not be published. Required fields are marked *