Topic 19 of 56 · Full Stack Advanced

Topic 4 : Working with asynchronous programming

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

Topic 4: Working with Asynchronous Programming

📖 6 min read · 🎯 beginner · 🧭 Prerequisites: running-emulator-and-simulator, using-rest

Why this matters

Here's the thing — when you first write JavaScript, everything runs one line at a time. That's fine until your code needs to fetch data from a database or read a file. Suddenly your entire program just waits, doing nothing, while that one slow task finishes. Users experience a frozen app. This lesson is about how JavaScript solves that problem. Callbacks, Promises, and async/await are three generations of the same idea: "don't wait — keep working, and I'll let you know when the data's ready." Understanding all three is how you write Node.js code that actually performs.

What You'll Learn

  • Understand why Node.js uses non-blocking I/O and how the event loop works
  • Handle asynchronous operations using callbacks, promises, and async/await
  • Run multiple async operations in parallel with Promise.all
  • Apply proper error handling patterns to async code
  • Schedule and control timed tasks with setTimeout and setInterval

The Analogy

Think of a busy coffee shop. A synchronous barista would take one order, make it completely, hand it over, and only then take the next order — the whole queue waits. An asynchronous barista takes your order, starts the espresso machine (which runs on its own), then immediately takes the next customer's order. When the machine beeps, they finish your drink and hand it over. Node.js is that second barista: it kicks off slow work (file reads, network calls, timers) and keeps the counter moving, returning to finish each job only when 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 a previous one finishes. Rather than sitting idle while a file loads from disk, Node.js registers a callback and moves on.
  2. Event Loop — Handles asynchronous operations by delegating tasks to the appropriate system resources and executing callbacks when those tasks complete. It is the heartbeat of every Node.js process.
sequenceDiagram
    participant App as Your Code
    participant EL as Event Loop
    participant OS as OS / I/O

    App->>EL: fs.readFile('file.txt', callback)
    EL->>OS: Delegate I/O task
    App->>EL: (continues running other code)
    OS-->>EL: I/O complete — here's the data
    EL-->>App: Execute callback(data)

Chapter 2: Callbacks

Callbacks are the traditional way to handle asynchronous operations in Node.js. You pass a function as an argument; Node.js calls it back once the async work is done.

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

Run it:

node readFileCallback.js

Callbacks work, but nesting them (reading a file, then another, then another) creates deeply indented "callback hell" that is hard to read and maintain. That's exactly what promises were designed to solve.

Chapter 3: Promises

Promises represent the eventual completion (or failure) of an asynchronous operation and its resulting value. They flatten the nested structure of callbacks into a readable chain.

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

Run it:

node readFilePromise.js

The .then() handler runs when the operation succeeds; .catch() catches any rejection. You can chain multiple .then() calls to sequence operations without nesting.

Chapter 4: Async/Await

async/await is syntactic sugar built on top of promises. It lets you write asynchronous code that looks synchronous, making it far easier to follow the logic.

  • Marking a function async means it always returns a promise.
  • await pauses execution inside that function until the awaited promise settles — without blocking the event loop for the rest of the application.

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

Run it:

node readFileAsyncAwait.js

The try/catch block handles errors just like synchronous code, making the control flow obvious at a glance.

Chapter 5: Handling Multiple Asynchronous Operations

When you need to run several async tasks at the same time, Promise.all fires them all in parallel and waits for every one to finish before continuing.

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

Run it:

node readMultipleFiles.js

Promise.all resolves with an array of results in the same order as the input array, regardless of which promise finished first. If any promise rejects, the whole Promise.all rejects immediately.

Example: Reading Multiple Files with Async/Await + Promise.all

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

Run it:

node readMultipleFilesAsyncAwait.js

Array destructuring (const [data1, data2]) pairs neatly with Promise.all, giving each result a clear name without indexing into a results array.

Chapter 6: Handling Errors in Asynchronous Code

Unhandled promise rejections crash Node.js processes in newer versions. Always wrap await calls in try/catch.

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

Run it:

node errorHandlingAsyncAwait.js

Expected output:

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

err.message extracts just the human-readable part of the error object. Logging the full err object is useful during development; err.message (or a custom error response) is better for production output.

Chapter 7: Using setTimeout and setInterval

Node.js provides two global timer functions for scheduling work without blocking the event loop.

FunctionPurpose
setTimeout(fn, ms)Run fn once after ms milliseconds
setInterval(fn, ms)Run fn repeatedly every ms milliseconds
clearInterval(id)Stop a running interval

Example: Using setTimeout

setTimeoutExample.js:

console.log('Before timeout');

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

console.log('After timeout');

Run it:

node setTimeoutExample.js

Output:

Before timeout
After timeout
Executed after 2 seconds

Notice that 'After timeout' prints before the delayed message — Node.js kept running while the timer waited.

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

Run it:

node setIntervalExample.js

Output:

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

clearInterval receives the ID returned by setInterval and stops future executions. Without it the interval would run forever (or until the process exits).

🧪 Try It Yourself

Task: Build a small script that fetches two "files" concurrently using Promise.all and reports their combined character count.

  1. Create two text files:
echo "Hello from file one" > file1.txt
echo "Hello from file two, with a bit more text" > file2.txt
  1. Write countChars.js:
const fs = require('fs').promises;

async function countChars() {
    try {
        const [a, b] = await Promise.all([
            fs.readFile('file1.txt', 'utf8'),
            fs.readFile('file2.txt', 'utf8')
        ]);
        const total = a.length + b.length;
        console.log(`File 1: ${a.length} chars`);
        console.log(`File 2: ${b.length} chars`);
        console.log(`Total: ${total} chars`);
    } catch (err) {
        console.error('Something went wrong:', err.message);
    }
}

countChars();
  1. Run it:
node countChars.js

Success criterion: You should see three lines — the character count for each file and their combined total. Then deliberately rename one file to trigger the catch block and confirm the error message prints cleanly.

🔍 Checkpoint Quiz

Q1. What does "non-blocking I/O" mean in the context of Node.js?

A) The program throws an error if two files are opened at once B) Node.js waits for each I/O operation to complete before starting the next C) Node.js delegates I/O to the OS and continues executing other code while waiting D) Files can only be read synchronously using fs.readFileSync


Q2. Given this code, what is the printed order?

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 is wrong with the following code?

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

async function load() {
    const data = await fs.readFile('data.txt', 'utf8');
    console.log(data);
}

load();

A) async functions cannot use await B) There is no error handling — a missing file will cause an unhandled rejection C) fs.promises.readFile does not exist in Node.js D) Nothing; the code is correct as long as data.txt exists


Q4. You need to fetch data from three independent API endpoints and only proceed once all three have responded. Which approach is most appropriate?

A) Three sequential await calls, one after another B) Promise.all with the three fetch calls passed as an array C) Three separate setInterval calls polling each endpoint D) Nested .then() chains, one inside the other


A1. C — Non-blocking I/O means Node.js hands the slow work off to the OS and keeps running other code; the callback or promise resolves when the OS signals completion.

A2. C — setTimeout(..., 0) defers the callback to the next iteration of the event loop, so A and C both print synchronously first, then B fires.

A3. B — Without a try/catch (or a .catch() on the returned promise), a missing or unreadable file will produce an unhandled promise rejection, which crashes the process in Node.js 15+.

A4. B — Promise.all fires all three requests in parallel and resolves only when every promise has settled, minimizing total wait time compared to sequential await calls.

🪞 Recap

  • Node.js's non-blocking I/O and event loop let it handle many operations without waiting for each to finish sequentially.
  • Callbacks are the foundation of async Node.js but can lead to deeply nested, hard-to-read code.
  • Promises flatten async chains with .then() / .catch(), while async/await makes the same logic read like synchronous code.
  • Promise.all runs multiple async operations in parallel and waits for all of them, delivering results as an ordered array.
  • Always wrap await calls in try/catch — unhandled rejections crash modern Node.js processes.
  • setTimeout schedules a one-time delayed callback; setInterval repeats until clearInterval is called.

📚 Further Reading

Like this topic? It’s one of 56 in Full Stack Advanced.

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