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.
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 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...');
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 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);
});
err
) before processing the data.
// Output //
Error reading file: [Error: ENOENT: no such file or directory, open 'nonexistent.txt']
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);
});
});
});
// Output //
Error reading file: [Error: ENOENT: no such file or directory, open 'nonexistent.txt']
Promises offer a more structured way to handle asynchronous operations, avoiding callback hell by chaining operations.
const fs = require('fs').promises;
fs.readFile('example.txt', 'utf8')
.then(data => {
console.log(data);
})
.catch(err => {
console.error(err);
});
console.log('Reading file...');
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)
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);
});
then
returning a new Promise.
// Output //
(File content of file1.txt)
(File content of file2.txt)
(File content of file3.txt)
fs.readFile('nonexistent.txt', 'utf8')
.then(data => {
console.log(data);
})
.catch(err => {
console.error('Error reading file:', err);
});
catch
block.
// Output //
Error reading file: [Error: ENOENT: no such file or directory, open 'nonexistent.txt']
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...');
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)
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();
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)
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();
try/catch
for error handling, similar to synchronous code.
// Output //
Error reading file: [Error: ENOENT: no such file or directory, open 'nonexistent.txt']
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();
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)
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();
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)
catch
block.try/catch
, similar to synchronous error handling.then
.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.
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);
});
https.get
.fs.writeFile
.
// Output //
Data saved to posts.json
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);
});
fetchData(url)
: Returns a Promise that resolves with the fetched data.fs.promises.writeFile
.
// Output //
Data saved to posts.json
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();
async function saveData()
: Uses await
to fetch and save data in a sequential manner.try/catch
.
// Output //
Data saved to posts.json
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 !❤️