Asynchronous Programming

Asynchronous programming is a fundamental aspect of Node.js, enabling it to handle I/O operations efficiently without blocking the execution of other tasks. This chapter provides an in-depth exploration of asynchronous programming in Node.js, from basic concepts to advanced techniques. We'll cover callbacks, Promises, and async/await, with detailed examples and explanations to ensure a thorough understanding.

Basics of Asynchronous Programming

Asynchronous programming allows your code to start a potentially time-consuming task and move on to other tasks before the previous one finishes. This is crucial in Node.js for handling I/O operations like reading files, making network requests, or querying databases.

Callbacks

Callbacks are the simplest form of handling asynchronous operations. A callback is a function passed as an argument to another function, which is executed once the asynchronous operation completes.

				
					const fs = require('fs');

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

				
			

Explanation:

  • fs.readFile(file, encoding, callback): Reads a file asynchronously and invokes the callback upon completion.
  • console.log('Reading file...');: Executes immediately, while fs.readFile works in the background.
				
					// Output //
Reading file...
(File content of example.txt)

				
			

Handling Errors

Handling errors in callbacks is crucial for robust applications.

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

				
			

Explanation:

  • Checks for an error (err) before processing the data.
				
					// Output //
Error reading file: [Error: ENOENT: no such file or directory, open 'nonexistent.txt']

				
			

Callback Hell

When multiple asynchronous operations are nested, the code can become difficult to read and maintain, a situation known as “callback hell.”

				
					fs.readFile('file1.txt', 'utf8', (err, data1) => {
  if (err) return console.error(err);
  fs.readFile('file2.txt', 'utf8', (err, data2) => {
    if (err) return console.error(err);
    fs.readFile('file3.txt', 'utf8', (err, data3) => {
      if (err) return console.error(err);
      console.log(data1, data2, data3);
    });
  });
});

				
			

Explanation:

  • Nested callbacks lead to deeply indented code, making it hard to read and maintain.
				
					// Output //
Error reading file: [Error: ENOENT: no such file or directory, open 'nonexistent.txt']

				
			

Promises

Promises offer a more structured way to handle asynchronous operations, avoiding callback hell by chaining operations.

Basic Example

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

fs.readFile('example.txt', 'utf8')
  .then(data => {
    console.log(data);
  })
  .catch(err => {
    console.error(err);
  });
console.log('Reading file...');

				
			

Explanation:

  • fs.promises.readFile(file, encoding): Returns a Promise that resolves with the file content.
  • then(callback): Handles the resolved value.
  • catch(callback): Handles errors.
				
					// Output //
Reading file...
(File content of example.txt)

				
			

Chaining Promises

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

fs.readFile('file1.txt', 'utf8')
  .then(data1 => {
    return fs.readFile('file2.txt', 'utf8').then(data2 => ({ data1, data2 }));
  })
  .then(result => {
    return fs.readFile('file3.txt', 'utf8').then(data3 => ({ ...result, data3 }));
  })
  .then(({ data1, data2, data3 }) => {
    console.log(data1, data2, data3);
  })
  .catch(err => {
    console.error(err);
  });

				
			

Explanation:

  • Promises are chained together, each then returning a new Promise.
				
					// Output //
(File content of file1.txt)
(File content of file2.txt)
(File content of file3.txt)

				
			

Error Handling in Promises

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

				
			

Explanation:

  • Errors in any part of the Promise chain are caught in the catch block.
				
					// Output //
Error reading file: [Error: ENOENT: no such file or directory, open 'nonexistent.txt']

				
			

Async/Await

Async/await syntax is built on Promises and allows writing asynchronous code in a synchronous-like manner, improving readability and maintainability.

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

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

readFile();
console.log('Reading file...');

				
			

Explanation:

  • async function: Declares an asynchronous function.
  • await expression: Pauses the function execution until the Promise resolves.
  • try/catch: Handles errors.
				
					// Output //
Reading file...
(File content of example.txt)

				
			

Handling Multiple Asynchronous Operations

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

async function readFiles() {
  try {
    const data1 = await fs.readFile('file1.txt', 'utf8');
    const data2 = await fs.readFile('file2.txt', 'utf8');
    const data3 = await fs.readFile('file3.txt', 'utf8');
    console.log(data1, data2, data3);
  } catch (err) {
    console.error(err);
  }
}

readFiles();

				
			

Explanation:

  • Each await ensures the previous operation completes before starting the next.
				
					// Output //
(File content of file1.txt)
(File content of file2.txt)
(File content of file3.txt)

				
			

Error Handling with Async/Await

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

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

readFile();

				
			

Explanation:

  • Uses try/catch for error handling, similar to synchronous code.
				
					// Output //
Error reading file: [Error: ENOENT: no such file or directory, open 'nonexistent.txt']

				
			

Advanced Topics

Parallel Execution with Promises

Using Promise.all to run multiple asynchronous operations in parallel.

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

async function readFilesInParallel() {
  try {
    const [data1, data2, data3] = await Promise.all([
      fs.readFile('file1.txt', 'utf8'),
      fs.readFile('file2.txt', 'utf8'),
      fs.readFile('file3.txt', 'utf8')
    ]);
    console.log(data1, data2, data3);
  } catch (err) {
    console.error(err);
  }
}

readFilesInParallel();

				
			

Explanation:

  • Promise.all(array): Runs all Promises in parallel and waits for all to resolve.
				
					// Output //
(File content of file1.txt)
(File content of file2.txt)
(File content of file3.txt)

				
			

Sequential Execution with Async/Await

Ensuring operations are executed sequentially.

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

async function readFilesSequentially() {
  try {
    const data1 = await fs.readFile('file1.txt', 'utf8');
    const data2 = await fs.readFile('file2.txt', 'utf8');
    const data3 = await fs.readFile('file3.txt', 'utf8');
    console.log(data1, data2, data3);
  } catch (err) {
    console.error(err);
  }
}

readFilesSequentially();

				
			

Explanation:

  • Each await ensures the previous operation completes before starting the next.
				
					// Output //
(File content of file1.txt)
(File content of file2.txt)
(File content of file3.txt)

				
			

Comparison of Asynchronous Methods

Readability

  • Callbacks: Can become messy with nested calls (callback hell).
  • Promises: Cleaner with chaining, but still not as straightforward as async/await.
  • Async/Await: Most readable and closest to synchronous code.

Error Handling

  • Callbacks: Errors must be handled in each callback.
  • Promises: Errors can be caught in a single catch block.
  • Async/Await: Uses try/catch, similar to synchronous error handling.

Chaining

  • Callbacks: Difficult to manage and prone to callback hell.
  • Promises: Easier to chain operations with then.
  • Async/Await: Linear and straightforward, making it easier to follow the flow of asynchronous operations.

Real-World Examples

To further solidify your understanding, let’s look at a more complex real-world example that involves fetching data from an API and saving it to a file.

Using Callbacks

				
					const https = require('https');
const fs = require('fs');

https.get('https://jsonplaceholder.typicode.com/posts', (res) => {
  let data = '';

  res.on('data', (chunk) => {
    data += chunk;
  });

  res.on('end', () => {
    fs.writeFile('posts.json', data, (err) => {
      if (err) {
        return console.error(err);
      }
      console.log('Data saved to posts.json');
    });
  });

}).on('error', (err) => {
  console.error('Error fetching data:', err);
});

				
			

Explanation:

  • Fetches data from an API using https.get.
  • Collects the data in chunks and writes it to a file using fs.writeFile.
				
					// Output //
Data saved to posts.json

				
			

Using Promises

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

function fetchData(url) {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      let data = '';
      res.on('data', (chunk) => {
        data += chunk;
      });
      res.on('end', () => {
        resolve(data);
      });
    }).on('error', (err) => {
      reject(err);
    });
  });
}

fetchData('https://jsonplaceholder.typicode.com/posts')
  .then(data => fs.writeFile('posts.json', data))
  .then(() => {
    console.log('Data saved to posts.json');
  })
  .catch(err => {
    console.error('Error:', err);
  });

				
			

Explanation:

  • fetchData(url): Returns a Promise that resolves with the fetched data.
  • Data is saved to a file using fs.promises.writeFile.
				
					// Output //
Data saved to posts.json

				
			

Using Async/Await

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

async function fetchData(url) {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      let data = '';
      res.on('data', (chunk) => {
        data += chunk;
      });
      res.on('end', () => {
        resolve(data);
      });
    }).on('error', (err) => {
      reject(err);
    });
  });
}

async function saveData() {
  try {
    const data = await fetchData('https://jsonplaceholder.typicode.com/posts');
    await fs.writeFile('posts.json', data);
    console.log('Data saved to posts.json');
  } catch (err) {
    console.error('Error:', err);
  }
}

saveData();

				
			

Explanation:

  • async function saveData(): Uses await to fetch and save data in a sequential manner.
  • Error handling is done using try/catch.
				
					// Output //
Data saved to posts.json

				
			

Summary

  • Callbacks: Basic but can lead to callback hell with nested operations.
  • Promises: Provide a more manageable way to handle async operations with chaining and better error handling.
  • Async/Await: Built on Promises, offering the most readable and maintainable syntax for asynchronous code.

Asynchronous programming is essential in Node.js for building efficient, non-blocking applications. Understanding the different methods—callbacks, Promises, and async/await—provides a solid foundation for handling asynchronous operations.Happy coding !❤️

Table of Contents

Contact here

Copyright © 2025 Diginode

Made with ❤️ in India