Topic 4 of 18 · Node Expert

Topic 4 : Working with asynchronous programming

Lesson TL;DRTopic 4: Working with Asynchronous Programming 📖 6 min read · 🎯 intermediate · 🧭 Prerequisites: workingwithpackagelockjsontolockthenodemodulesversions, usingrest Why this matters Here's the thing —...
6 min read·intermediate·nodejs · async-await · promises · callbacks

Topic 4: Working with Asynchronous Programming

📖 6 min read · 🎯 intermediate · 🧭 Prerequisites: working-with-package-lock-json-to-lock-the-node-modules-versions, using-rest

Why this matters

Here's the thing — have you ever clicked a button in an app and the whole screen just froze? Nothing you could do, just waiting. That's what happens when your Node.js code reads a file, calls an API, or hits a database synchronously — it stops everything until the job is done. In a real server handling hundreds of users, that's a disaster. Asynchronous programming is how we fix it. With callbacks, promises, and async/await, your code can kick off a task and keep working while it waits — no freezing, no blocking, just smooth execution.

What You'll Learn

  • Understand how Node.js's non-blocking I/O model and event loop enable asynchronous execution
  • Handle async operations the traditional way using callbacks
  • Upgrade to promises and the .then/.catch chain pattern
  • Write cleaner async code using async/await syntax
  • Run multiple async operations in parallel with Promise.all
  • Handle errors correctly in all three async styles
  • Schedule deferred and repeating tasks with setTimeout and setInterval

The Analogy

Think of a busy coffee shop. A synchronous barista would take one order, make the drink start to finish, hand it over, then — and only then — take the next order. The whole queue waits. An asynchronous barista takes your order, starts the espresso machine, and immediately turns to the next customer while the machine does its work. When the machine beeps (the callback fires), the barista finishes your drink. Node.js is that second barista: it kicks off slow operations (disk reads, network calls) and keeps processing other work until the result is ready.

Chapter 1: Understanding Asynchronous Programming

Asynchronous programming allows tasks to run concurrently, improving performance and responsiveness. Node.js, with its non-blocking I/O model, heavily relies on asynchronous programming to handle multiple operations efficiently.

Key Concepts

  1. Non-blocking I/O — Allows other operations to continue before the previous one finishes. When Node asks the OS to read a file, it registers a callback and moves on; it does not sit idle.
  2. Event Loop — The heart of Node's async model. It continuously monitors a queue of completed operations and executes their registered callbacks when tasks are finished, keeping the single-threaded runtime from stalling.
sequenceDiagram
    participant App
    participant EventLoop
    participant OS

    App->>EventLoop: fs.readFile('example.txt', cb)
    EventLoop->>OS: Delegate file read
    App->>EventLoop: Continue executing next lines
    OS-->>EventLoop: File read complete
    EventLoop->>App: Execute cb(null, data)

Chapter 2: Callbacks

Callbacks are the traditional way to handle asynchronous operations in Node.js — you pass a function as the last argument, and Node calls it when the operation completes.

Example: Reading a File with a Callback

readFileCallback.js:

const fs = require('fs');

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

Running the application:

node readFileCallback.js

The callback receives two arguments by Node convention: err first, then the result. Always check err before using data — if you ignore it, a missing file will silently crash your logic downstream.

Chapter 3: Promises

Promises provide a more elegant way to handle asynchronous operations compared to callbacks. A Promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Node's fs.promises API returns promises directly.

Example: Reading a File with Promises

readFilePromise.js:

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);
    });

Running the application:

node readFilePromise.js

The .then() handler runs on success; .catch() handles any rejection. Chaining multiple .then() calls avoids the deeply nested "callback hell" that plagues raw callback code.

Chapter 4: Async/Await

async/await is syntactic sugar built on top of promises. It lets you write asynchronous code that reads like synchronous code, making intent clearer and error paths easier to reason about.

Example: Reading a File with Async/Await

readFileAsyncAwait.js:

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

async function readFile() {
    try {
        const data = await fs.readFile('example.txt', 'utf8');
        console.log('File content:', data);
    } catch (err) {
        console.error('Error reading file:', err);
    }
}

readFile();

Running the application:

node readFileAsyncAwait.js

await pauses execution inside the async function until the promise resolves — but it does not block the event loop. Other callbacks and timers still run while this function is suspended.

Chapter 5: Handling Multiple Asynchronous Operations

When you need results from several independent async operations, running them sequentially wastes time. Promise.all fires them all at once and waits for every promise to settle.

Example: Reading Multiple Files with Promise.all

readMultipleFiles.js:

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

Promise.all([
    fs.readFile('example1.txt', 'utf8'),
    fs.readFile('example2.txt', 'utf8')
])
    .then((results) => {
        console.log('File 1 content:', results[0]);
        console.log('File 2 content:', results[1]);
    })
    .catch((err) => {
        console.error('Error reading files:', err);
    });

Running the application:

node readMultipleFiles.js

Example: Reading Multiple Files with Async/Await

readMultipleFilesAsyncAwait.js:

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

async function readFiles() {
    try {
        const [data1, data2] = await Promise.all([
            fs.readFile('example1.txt', 'utf8'),
            fs.readFile('example2.txt', 'utf8')
        ]);
        console.log('File 1 content:', data1);
        console.log('File 2 content:', data2);
    } catch (err) {
        console.error('Error reading files:', err);
    }
}

readFiles();

Running the application:

node readMultipleFilesAsyncAwait.js

Promise.all rejects immediately if any promise in the array rejects. If you need partial results even when some fail, use Promise.allSettled instead.

Chapter 6: Handling Errors in Asynchronous Code

Proper error handling is essential — an unhandled rejection in async code can crash a Node process silently in older versions or emit an UnhandledPromiseRejection warning in newer ones.

Example: Error Handling with Async/Await

errorHandlingAsyncAwait.js:

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

async function readFile() {
    try {
        const data = await fs.readFile('nonexistent.txt', 'utf8');
        console.log('File content:', data);
    } catch (err) {
        console.error('Error reading file:', err.message);
    }
}

readFile();

Running the application:

node errorHandlingAsyncAwait.js

Output:

Error reading file: ENOENT: no such file or directory, open 'nonexistent.txt'

The try/catch block around await expressions catches both synchronous throws and rejected promises, giving you one unified error-handling path.

Chapter 7: Using setTimeout and setInterval

Node exposes setTimeout and setInterval for scheduling tasks. Both are non-blocking — registering a timer does not pause execution; the callback fires later via the event loop.

Example: Using setTimeout

setTimeoutExample.js:

console.log('Before timeout');

setTimeout(() => {
    console.log('Executed after 2 seconds');
}, 2000);

console.log('After timeout');

Running the application:

node setTimeoutExample.js

Output:

Before timeout
After timeout
Executed after 2 seconds

Notice that 'After timeout' prints immediately — setTimeout registers the callback and returns; it does not block the next line.

Example: Using setInterval

setIntervalExample.js:

let counter = 0;

const intervalId = setInterval(() => {
    counter += 1;
    console.log(`Counter: ${counter}`);
    if (counter === 5) {
        clearInterval(intervalId);
        console.log('Interval cleared');
    }
}, 1000);

Running the application:

node setIntervalExample.js

Output:

Counter: 1
Counter: 2
Counter: 3
Counter: 4
Counter: 5
Interval cleared

Always store the return value of setInterval so you can call clearInterval when you're done. A forgotten interval keeps the Node process alive indefinitely.

🧪 Try It Yourself

Task: Write a script that reads two files concurrently and writes a combined summary to a third file.

  1. Create file1.txt containing Hello and file2.txt containing World.
  2. Use Promise.all + async/await to read both files simultaneously.
  3. Write the combined content (Hello World) to combined.txt using fs.promises.writeFile.

Success criterion: Running the script produces combined.txt with the text Hello World and logs Done! combined.txt written. to the console.

Starter snippet:

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

async function combineFiles() {
    try {
        const [data1, data2] = await Promise.all([
            fs.readFile('file1.txt', 'utf8'),
            fs.readFile('file2.txt', 'utf8')
        ]);
        const combined = `${data1.trim()} ${data2.trim()}`;
        await fs.writeFile('combined.txt', combined, 'utf8');
        console.log('Done! combined.txt written.');
    } catch (err) {
        console.error('Something went wrong:', err.message);
    }
}

combineFiles();

🔍 Checkpoint Quiz

Q1. What is the role of the Event Loop in Node.js asynchronous programming?

A) It runs all code on multiple CPU threads simultaneously
B) It monitors completed async operations and executes their callbacks
C) It pauses execution until each I/O operation finishes
D) It converts callback-based APIs into promises automatically

Q2. Given the following code, what is the order of output?

console.log('A');
setTimeout(() => console.log('B'), 0);
console.log('C');

A) A, B, C
B) B, A, C
C) A, C, B
D) C, A, B

Q3. What happens if one of the promises passed to Promise.all rejects?

A) The remaining promises are cancelled and the array returns partial results
B) The rejection is silently ignored and other results are returned
C) Promise.all immediately rejects with that error, even if other promises succeed
D) Promise.all waits for all promises to finish before rejecting

Q4. Rewrite this callback-based code using async/await:

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

How would you handle the error in the async/await version?

A1. B — The event loop continuously checks the queue for completed async operations and invokes their registered callbacks; it is the mechanism that makes single-threaded Node.js non-blocking.

A2. C — 'A' and 'C' execute synchronously in order. Even with a 0ms delay, setTimeout registers a callback that runs after the current synchronous call stack is clear, so 'B' prints last.

A3. C — Promise.all fails fast: the moment any single promise rejects, the whole Promise.all rejects with that reason. Use Promise.allSettled if you need results from the promises that did succeed.

A4. Wrap the await call in a try/catch block:

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

async function readData() {
    try {
        const data = await fs.readFile('data.txt', 'utf8');
        console.log(data);
    } catch (err) {
        console.error(err);
    }
}

readData();

The catch block catches both OS-level errors (like ENOENT) and any unexpected thrown values, mirroring the if (err) check in the callback version.

🪞 Recap

  • Node.js uses a non-blocking I/O model and an event loop to handle async operations without stalling the process.
  • Callbacks are the foundational pattern but nest poorly; promises introduced chainable .then/.catch to flatten async flows.
  • async/await is syntactic sugar over promises that lets async code read like sequential synchronous code.
  • Promise.all runs multiple independent promises in parallel and resolves when all succeed (or rejects on the first failure).
  • Always handle errors in async code — via callback's err parameter, .catch(), or try/catch around await.
  • setTimeout and setInterval schedule deferred and repeating tasks through the event loop; clearInterval is required to stop a running interval.

📚 Further Reading

Like this topic? It’s one of 18 in Node Expert.

Block your seat for ₹2,500 and join the next cohort.