Node.js Async — Callbacks, Promises, Async/Await & the Event Loop
Node.js is asynchronous by design — instead of waiting for operations to complete, it registers callbacks and continues executing, then returns to handle results when ready. Understanding this model is essential for writing performant Node.js applications.
What You’ll Learn
By the end of this tutorial, you’ll understand the event loop, convert callback-based code to promises, use async/await effectively, and handle errors in asynchronous code.
Why Async Matters
Asynchronous programming is what makes Node.js fast. Durga Antivirus Pro uses async patterns to scan multiple files simultaneously without blocking the interface. Doda Browser uses async for parallel bookmark syncing and history lookups. DodaZIP processes file operations asynchronously so large archives don’t freeze the application. Mastering async is the difference between a slow, blocked application and a fast, responsive one.
Security note: Understanding Nodejs Async helps build more secure applications — a core principle at DodaTech, where tools like Durga Antivirus Pro and Doda Browser rely on solid implementation practices.
Async Learning Path
flowchart LR
A[Node.js Basics] --> B[Async JS]
B --> C[Core Modules]
C --> D[Express.js]
B --> E{You Are Here}
style E fill:#f90,color:#fff
Understanding Async (The “Why” First)
Think of asynchronous code like ordering coffee at a busy café. You place your order (start async operation), get a buzzer (callback/promise), and sit down (continue other work). When your coffee is ready (operation completes), the buzzer buzzes (callback fires) and you pick it up. If you had to stand at the counter and wait (synchronous), the entire café would grind to a halt.
flowchart LR
A[Main Thread] -->|1. Start| B[Async Operation]
B -->|2. Register| C[Event Loop]
C -->|3. Continue| A
B -->|4. Complete| D[Callback Queue]
D -->|5. Execute| E[Callback]
Callbacks — The Original Pattern
const fs = require("node:fs");
// Error-first callback convention
fs.readFile("data.txt", "utf8", (err, data) => {
if (err) {
console.error("Error:", err.message);
return;
}
console.log("File content:", data);
});
console.log("Reading file..."); // Runs BEFORE the callback
The “error-first” callback pattern — first parameter is always an error (or null), second is the result — is a Node.js convention you’ll see everywhere.
Callback Hell — What to Avoid
// ❌ Nested callbacks — unreadable
fs.readFile("a.txt", (err, a) => {
fs.readFile("b.txt", (err, b) => {
fs.readFile("c.txt", (err, c) => {
console.log(a + b + c);
});
});
});This is “callback hell” — pyramid-shaped nesting that’s hard to read, debug, and maintain.
Promises — A Cleaner Way
const fs = require("node:fs/promises");
// Chaining with .then() and .catch()
fs.readFile("data.txt", "utf8")
.then(data => console.log(data))
.catch(err => console.error(err));
// Run multiple in parallel
Promise.all([
fs.readFile("a.txt", "utf8"),
fs.readFile("b.txt", "utf8"),
]).then(([a, b]) => console.log(a + b));Promises represent a value that may be available now, later, or never. They have three states: pending, fulfilled, rejected. Promise.all runs multiple operations in parallel and waits for all to complete.
Async/Await — Sugar on Top
const fs = require("node:fs/promises");
async function readFiles() {
try {
const a = await fs.readFile("a.txt", "utf8");
const b = await fs.readFile("b.txt", "utf8");
console.log(a + b);
} catch (err) {
console.error("Error:", err.message);
}
}
// Parallel with async/await
async function readFilesParallel() {
const [a, b] = await Promise.all([
fs.readFile("a.txt", "utf8"),
fs.readFile("b.txt", "utf8"),
]);
console.log(a + b);
}async/await is syntactic sugar over promises — it makes asynchronous code read like synchronous code. The await keyword pauses the function (not the whole program!) until the promise resolves.
The Event Loop
console.log("1. Start");
setTimeout(() => console.log("2. Timeout"), 0);
setImmediate(() => console.log("3. Immediate"));
process.nextTick(() => console.log("4. NextTick"));
Promise.resolve().then(() => console.log("5. Promise"));
console.log("6. End");
// Output:
// 1. Start
// 6. End
// 4. NextTick ← microtask queue (highest priority)
// 5. Promise ← microtask queue
// 2. Timeout ← timers phase
// 3. Immediate ← check phase
The event loop phases in order: timers → I/O callbacks → poll → check (setImmediate) → close. Microtasks (nextTick, Promise callbacks) run between each phase.
Common Mistakes
1. Using async/await Without try/catch
Unhandled promise rejections terminate the process in modern Node.js. Always wrap await calls.
2. Serial Execution When Parallel is Possible
// ❌ Slow — sequential
const a = await readFile("a.txt");
const b = await readFile("b.txt");
// ✅ Fast — parallel
const [a, b] = await Promise.all([readFile("a.txt"), readFile("b.txt")]);3. Forgetting to Return the Promise
Without returning, callers can’t await or chain. Always return promises from async functions.
4. Using process.nextTick to Defer I/O
Use setImmediate for I/O deferral. nextTick is for microtasks and can starve the event loop.
5. Not Handling Stream Errors
Streams emit ’error’ events that crash the process if unhandled. Always attach .on('error', handler).
Practice Questions
1. What’s the event loop?
A mechanism that handles asynchronous callbacks in phases — timers, I/O, poll, check, close. It allows Node.js to be non-blocking despite single-threaded execution.
2. Difference between nextTick and setImmediate?
nextTick runs before the next event loop phase (microtask priority). setImmediate runs in the check phase after I/O. nextTick can starve I/O if used recursively.
3. How do you convert callbacks to promises?
Use util.promisify for Node.js callback-style functions, or wrap in new Promise((resolve, reject) => { ... }).
4. When should you use Promise.all?
When you have multiple independent async operations that can run in parallel. It fails fast — if any promise rejects, the whole thing rejects.
5. Challenge: Write an async function that reads 3 files in parallel and concatenates their content.
const fs = require("node:fs/promises");
async function concatFiles(...paths) {
const contents = await Promise.all(
paths.map(p => fs.readFile(p, "utf8").catch(() => ""))
);
return contents.join("\n");
}FAQ
Try It Yourself
Create a file reader that reads multiple files in parallel with error handling:
#!/usr/bin/env node
const fs = require("node:fs/promises");
async function readFiles(filePaths) {
const results = await Promise.all(
filePaths.map(async (filePath) => {
try {
const content = await fs.readFile(filePath, "utf8");
return { file: filePath, content, status: "ok" };
} catch (err) {
return { file: filePath, error: err.message, status: "error" };
}
})
);
results.forEach(r => {
if (r.status === "ok") console.log(`\n=== ${r.file} ===\n${r.content}`);
else console.error(`\n⚠ ${r.file}: ${r.error}`);
});
}
const files = process.argv.slice(2);
if (files.length === 0) {
console.error("Usage: node read-files.js <file1> <file2> ...");
process.exit(1);
}
readFiles(files);What’s Next
| Lesson | Description |
|---|---|
| https://tutorials.dodatech.com/backend/nodejs/nodejs-core-modules/ | File system, HTTP, streams |
| https://tutorials.dodatech.com/backend/nodejs/express/ | Express.js web framework |
| https://tutorials.dodatech.com/backend/nodejs/nodejs-database/ | Async database operations |
| JavaScript | Modern JS features |
| REST API | Building async APIs |
What’s Next
Congratulations on completing this Nodejs Async tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro