Node.js Callbacks Concept - Understanding Asynchronous Programming
Callbacks are fundamental to Node.js's asynchronous programming model. They are functions that are passed as arguments to other functions and are executed at a later time, typically after an asynchronous operation completes.
What are Callbacks?
A callback is a function that is passed as an argument to another function and is executed at a later time. In Node.js, callbacks are commonly used for handling asynchronous operations.
Basic Callback Example
// Basic callback function
function greet(name, callback) {
console.log(`Hello, ${name}!`);
callback(); // Execute the callback
}
// Callback function
function sayGoodbye() {
console.log('Goodbye!');
}
// Using the callback
greet('John', sayGoodbye);
// Output:
// Hello, John!
// Goodbye!
Anonymous Callback
// Using anonymous callback
greet('Alice', function() {
console.log('See you later!');
});
// Using arrow function callback
greet('Bob', () => {
console.log('Take care!');
});
Asynchronous Callbacks
File System Example
const fs = require('fs');
// Asynchronous file read with callback
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
return;
}
console.log('File content:', data);
});
console.log('This runs before file is read');
HTTP Request Example
const https = require('https');
// Asynchronous HTTP request with callback
https.get('https://api.github.com/users/octocat', (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('Response:', JSON.parse(data));
});
}).on('error', (err) => {
console.error('Error:', err);
});
Error Handling in Callbacks
Error-First Callback Pattern
Node.js follows the error-first callback pattern where the first parameter is always an error object.
function asyncOperation(data, callback) {
// Simulate async operation
setTimeout(() => {
if (Math.random() > 0.5) {
// Success case
callback(null, `Processed: ${data}`);
} else {
// Error case
callback(new Error('Something went wrong'));
}
}, 1000);
}
// Using the callback
asyncOperation('test data', (err, result) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('Result:', result);
});
File Operations with Error Handling
const fs = require('fs');
// Read file with proper error handling
fs.readFile('nonexistent.txt', 'utf8', (err, data) => {
if (err) {
if (err.code === 'ENOENT') {
console.error('File not found');
} else {
console.error('Error reading file:', err.message);
}
return;
}
console.log('File content:', data);
});
Callback Patterns
Sequential Callbacks
const fs = require('fs');
// Sequential file operations
fs.readFile('file1.txt', 'utf8', (err1, data1) => {
if (err1) {
console.error('Error reading file1:', err1);
return;
}
console.log('File1 content:', data1);
fs.readFile('file2.txt', 'utf8', (err2, data2) => {
if (err2) {
console.error('Error reading file2:', err2);
return;
}
console.log('File2 content:', data2);
// Process both files
console.log('Both files processed');
});
});
Parallel Callbacks
const fs = require('fs');
let completed = 0;
const results = [];
function checkComplete() {
completed++;
if (completed === 2) {
console.log('All files processed:', results);
}
}
// Read files in parallel
fs.readFile('file1.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error reading file1:', err);
} else {
results.push({ file: 'file1.txt', data });
}
checkComplete();
});
fs.readFile('file2.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error reading file2:', err);
} else {
results.push({ file: 'file2.txt', data });
}
checkComplete();
});
Callback Hell (Pyramid of Doom)
The Problem
// Callback hell example
fs.readFile('file1.txt', 'utf8', (err1, data1) => {
if (err1) {
console.error('Error reading file1:', err1);
return;
}
fs.readFile('file2.txt', 'utf8', (err2, data2) => {
if (err2) {
console.error('Error reading file2:', err2);
return;
}
fs.readFile('file3.txt', 'utf8', (err3, data3) => {
if (err3) {
console.error('Error reading file3:', err3);
return;
}
// Process all three files
console.log('All files processed');
});
});
});
Solutions to Callback Hell
1. Named Functions
function processFile1(err1, data1) {
if (err1) {
console.error('Error reading file1:', err1);
return;
}
fs.readFile('file2.txt', 'utf8', processFile2);
}
function processFile2(err2, data2) {
if (err2) {
console.error('Error reading file2:', err2);
return;
}
fs.readFile('file3.txt', 'utf8', processFile3);
}
function processFile3(err3, data3) {
if (err3) {
console.error('Error reading file3:', err3);
return;
}
console.log('All files processed');
}
// Start the chain
fs.readFile('file1.txt', 'utf8', processFile1);
2. Async Library
const async = require('async');
async.series([
(callback) => {
fs.readFile('file1.txt', 'utf8', callback);
},
(callback) => {
fs.readFile('file2.txt', 'utf8', callback);
},
(callback) => {
fs.readFile('file3.txt', 'utf8', callback);
}
], (err, results) => {
if (err) {
console.error('Error:', err);
return;
}
console.log('All files processed:', results);
});
3. Promises
const fs = require('fs').promises;
async function processFiles() {
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('All files processed');
} catch (err) {
console.error('Error:', err);
}
}
processFiles();
Custom Callback Functions
Creating Callback Functions
// Function that accepts a callback
function fetchUserData(userId, callback) {
// Simulate database query
setTimeout(() => {
if (userId > 0) {
const user = {
id: userId,
name: 'John Doe',
email: '[email protected]'
};
callback(null, user);
} else {
callback(new Error('Invalid user ID'));
}
}, 1000);
}
// Using the callback
fetchUserData(123, (err, user) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('User:', user);
});
Callback with Multiple Parameters
function calculate(a, b, operation, callback) {
setTimeout(() => {
let result;
let error = null;
try {
switch (operation) {
case 'add':
result = a + b;
break;
case 'subtract':
result = a - b;
break;
case 'multiply':
result = a * b;
break;
case 'divide':
if (b === 0) {
error = new Error('Division by zero');
} else {
result = a / b;
}
break;
default:
error = new Error('Invalid operation');
}
} catch (err) {
error = err;
}
callback(error, result);
}, 100);
}
// Using the callback
calculate(10, 5, 'divide', (err, result) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('Result:', result);
});
Callback Best Practices
1. Always Handle Errors
// Good: Always check for errors
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error:', err);
return;
}
// Process data
});
// Bad: Ignoring errors
fs.readFile('file.txt', 'utf8', (err, data) => {
// Process data without checking for errors
});
2. Use Consistent Error Handling
function handleError(err, operation) {
if (err) {
console.error(`Error in ${operation}:`, err.message);
return true; // Indicates error occurred
}
return false; // No error
}
fs.readFile('file1.txt', 'utf8', (err, data) => {
if (handleError(err, 'reading file1')) return;
console.log('File1 processed');
});
3. Avoid Callback Hell
// Good: Use named functions
function step1(callback) {
// Do something
callback(null, 'result1');
}
function step2(result1, callback) {
// Do something with result1
callback(null, 'result2');
}
function step3(result2, callback) {
// Do something with result2
callback(null, 'final result');
}
// Chain the callbacks
step1((err, result1) => {
if (err) return console.error(err);
step2(result1, (err, result2) => {
if (err) return console.error(err);
step3(result2, (err, finalResult) => {
if (err) return console.error(err);
console.log('Final result:', finalResult);
});
});
});
4. Use Promises for Complex Operations
// Convert callback to Promise
function readFilePromise(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
// Use with async/await
async function processFiles() {
try {
const data1 = await readFilePromise('file1.txt');
const data2 = await readFilePromise('file2.txt');
console.log('Both files processed');
} catch (err) {
console.error('Error:', err);
}
}
Common Callback Patterns
1. Waterfall Pattern
const async = require('async');
async.waterfall([
(callback) => {
fs.readFile('file1.txt', 'utf8', callback);
},
(data1, callback) => {
fs.readFile('file2.txt', 'utf8', (err, data2) => {
callback(err, data1, data2);
});
},
(data1, data2, callback) => {
// Process both files
const result = data1 + data2;
callback(null, result);
}
], (err, result) => {
if (err) {
console.error('Error:', err);
return;
}
console.log('Result:', result);
});
2. Parallel Pattern
async.parallel([
(callback) => {
fs.readFile('file1.txt', 'utf8', callback);
},
(callback) => {
fs.readFile('file2.txt', 'utf8', callback);
},
(callback) => {
fs.readFile('file3.txt', 'utf8', callback);
}
], (err, results) => {
if (err) {
console.error('Error:', err);
return;
}
console.log('All files processed:', results);
});
3. Series Pattern
async.series([
(callback) => {
console.log('Step 1');
callback(null, 'result1');
},
(callback) => {
console.log('Step 2');
callback(null, 'result2');
},
(callback) => {
console.log('Step 3');
callback(null, 'result3');
}
], (err, results) => {
if (err) {
console.error('Error:', err);
return;
}
console.log('All steps completed:', results);
});
Callback vs Promises vs Async/Await
Callbacks
// Callback approach
function getData(callback) {
fs.readFile('data.json', 'utf8', (err, data) => {
if (err) {
callback(err);
return;
}
callback(null, JSON.parse(data));
});
}
getData((err, data) => {
if (err) {
console.error('Error:', err);
return;
}
console.log('Data:', data);
});
Promises
// Promise approach
function getData() {
return fs.promises.readFile('data.json', 'utf8')
.then(data => JSON.parse(data));
}
getData()
.then(data => console.log('Data:', data))
.catch(err => console.error('Error:', err));
Async/Await
// Async/await approach
async function getData() {
try {
const data = await fs.promises.readFile('data.json', 'utf8');
return JSON.parse(data);
} catch (err) {
throw err;
}
}
async function main() {
try {
const data = await getData();
console.log('Data:', data);
} catch (err) {
console.error('Error:', err);
}
}
main();
Next Steps
Now that you understand callbacks, you're ready to:
- Node.js - Upload Files - Learn file upload handling
- Node.js - Send an Email - Implement email functionality
- Node.js - Events - Learn event-driven programming
- Node.js - Event Loop - Understand the event loop
Callbacks Mastery Complete! You now understand how to use callbacks effectively in Node.js for asynchronous programming. Callbacks are the foundation of Node.js's event-driven architecture!