Skip to main content

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:

  1. Node.js - Upload Files - Learn file upload handling
  2. Node.js - Send an Email - Implement email functionality
  3. Node.js - Events - Learn event-driven programming
  4. 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!