Async Code in Node.js: Callbacks and Promises

Node.js relies on asynchronous programming to handle I/O operations efficiently without blocking the main thread. This blog explores why async code matters, using file reading examples to compare callbacks and promises.
Why Async Exists
Node.js runs on a single-threaded event loop, making non-blocking I/O essential for performance. Synchronous operations like fs.readFileSync() halt execution until complete, which works for simple scripts but fails under load—imagine a web server freezing while reading files. Async code lets the event loop process other tasks, like handling HTTP requests, while waiting for disk or network operations.
Callback Example
Start with a basic file read using callbacks from the fs module.
const fs = require('node:fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error:', err);
return;
}
console.log(data);
});
Step-by-step flow:
readFileinitiates the async operation and immediately returns control to the event loop.The OS completes the read; Node.js queues the callback.
Event loop executes the callback, checking
errfirst (error-first convention).nodejs+1If no error,
dataholds the file content.
This non-blocking approach shines for servers but gets messy with multiple operations.
Nested Callback Problems
Chaining callbacks creates "callback hell"—deeply nested code that's hard to read and maintain.
fs.readFile('file1.txt', 'utf8', (err1, data1) => {
if (err1) return console.error(err1);
fs.readFile('file2.txt', 'utf8', (err2, data2) => {
if (err2) return console.error(err2);
fs.writeFile('output.txt', data1 + data2, (err3) => {
if (err3) return console.error(err3);
console.log('Done!');
});
});
});
Error handling repeats, indentation grows, and debugging becomes painful.
Promise-Based Handling
Promises replace nested callbacks with chaining. Use Node's fs.promises for built-in support.
const fs = require('node:fs/promises');
async function processFiles() {
try {
const data1 = await fs.readFile('file1.txt', 'utf8');
const data2 = await fs.readFile('file2.txt', 'utf8');
await fs.writeFile('output.txt', data1 + data2);
console.log('Done!');
} catch (err) {
console.error('Error:', err);
}
}
processFiles();
Promises represent eventual completion: pending, fulfilled (.then()), or rejected (.catch()).



