Callbacks & Error-First Pattern

📖 Concept

Callbacks are the original async pattern in Node.js. A callback is a function passed as an argument to another function, invoked when the operation completes.

The error-first callback convention: Node.js follows a strict convention: the callback's first argument is always the error (or null if no error). This is called the error-first or Node-style callback pattern.

fs.readFile('file.txt', (error, data) => {
  if (error) {
    // Handle error
    return;
  }
  // Use data
});

Why error-first?

  • Consistent API across all Node.js core modules
  • Forces developers to handle errors before processing data
  • Enables tools and libraries to automatically wrap callbacks

The callback problem — "Callback Hell" (Pyramid of Doom): When you chain async operations, callbacks nest deeper and deeper:

getUser(id, (err, user) => {
  getOrders(user.id, (err, orders) => {
    getOrderDetails(orders[0].id, (err, details) => {
      // 3 levels deep... and growing
    });
  });
});

Solutions to callback hell:

  1. Named functions — extract each callback into a named function
  2. Promises — chain with .then() instead of nesting
  3. async/await — write async code that looks synchronous
  4. Librariesasync.js provides utilities like async.waterfall, async.parallel

When callbacks are still used:

  • Legacy Node.js APIs (before Promises were standard)
  • Event handlers (EventEmitter, Stream)
  • Performance-critical code (callbacks have slightly less overhead than Promises)
  • Third-party libraries that haven't migrated to Promises

🏠 Real-world analogy: A callback is like leaving your phone number with a restaurant when the table isn't ready — they "call back" when it's your turn. Error-first is like the restaurant saying "Sorry, we're full" before giving you a table number.

💻 Code Example

codeTap to expand ⛶
1// Callbacks — The Foundation of Node.js Async
2
3const fs = require("fs");
4const path = require("path");
5
6// 1. Basic error-first callback
7fs.readFile(path.join(__dirname, "example.txt"), "utf-8", (err, data) => {
8 if (err) {
9 if (err.code === "ENOENT") {
10 console.log("File not found");
11 } else {
12 console.error("Read error:", err.message);
13 }
14 return; // ← Always return after handling errors!
15 }
16 console.log("File content:", data);
17});
18
19// 2. Callback hell — ❌ BAD
20function getUserDataBad(userId) {
21 getUser(userId, (err, user) => {
22 if (err) return console.error(err);
23 getOrders(user.id, (err, orders) => {
24 if (err) return console.error(err);
25 getShippingStatus(orders[0].id, (err, status) => {
26 if (err) return console.error(err);
27 console.log(`User: ${user.name}, Status: ${status}`);
28 // More nesting? This becomes unreadable...
29 });
30 });
31 });
32}
33
34// 3. Named functions — ✅ Flattened callbacks
35function getUserDataGood(userId) {
36 getUser(userId, handleUser);
37}
38
39function handleUser(err, user) {
40 if (err) return console.error(err);
41 getOrders(user.id, (err, orders) => handleOrders(err, orders, user));
42}
43
44function handleOrders(err, orders, user) {
45 if (err) return console.error(err);
46 console.log(`User ${user.name} has ${orders.length} orders`);
47}
48
49// 4. Creating callback-based functions
50function readJsonFile(filePath, callback) {
51 fs.readFile(filePath, "utf-8", (err, data) => {
52 if (err) return callback(err, null);
53
54 try {
55 const json = JSON.parse(data);
56 callback(null, json); // Success: error is null
57 } catch (parseErr) {
58 callback(new Error(`Invalid JSON in ${filePath}: ${parseErr.message}`), null);
59 }
60 });
61}
62
63// Usage
64readJsonFile("config.json", (err, config) => {
65 if (err) {
66 console.error("Failed:", err.message);
67 return;
68 }
69 console.log("Config loaded:", config);
70});
71
72// 5. Parallel execution with callbacks (manual)
73function fetchAllData(callback) {
74 let completed = 0;
75 const results = {};
76 const errors = [];
77
78 function checkDone() {
79 completed++;
80 if (completed === 3) {
81 if (errors.length > 0) return callback(errors[0], null);
82 callback(null, results);
83 }
84 }
85
86 fs.readFile("users.json", "utf-8", (err, data) => {
87 if (err) errors.push(err);
88 else results.users = JSON.parse(data);
89 checkDone();
90 });
91
92 fs.readFile("posts.json", "utf-8", (err, data) => {
93 if (err) errors.push(err);
94 else results.posts = JSON.parse(data);
95 checkDone();
96 });
97
98 fs.readFile("comments.json", "utf-8", (err, data) => {
99 if (err) errors.push(err);
100 else results.comments = JSON.parse(data);
101 checkDone();
102 });
103}
104
105// 6. Converting callbacks to Promises
106const { promisify } = require("util");
107const readFileAsync = promisify(fs.readFile);
108// Now: const data = await readFileAsync("file.txt", "utf-8");
109
110// Mock functions for examples above
111function getUser(id, cb) { cb(null, { id, name: "Alice" }); }
112function getOrders(userId, cb) { cb(null, [{ id: 1 }, { id: 2 }]); }
113function getShippingStatus(orderId, cb) { cb(null, "shipped"); }

🏋️ Practice Exercise

Exercises:

  1. Write a callback-based function that reads a directory and returns only .js files
  2. Create a readJsonFile function with proper error-first callback handling
  3. Chain 3 file operations using callbacks — then refactor to eliminate the nesting
  4. Implement a parallel file reader that reads 5 files concurrently and returns results in order
  5. Use util.promisify() to convert 3 callback-based Node.js APIs to Promise-based ones
  6. Build a retry function that attempts a callback operation up to 3 times before failing

⚠️ Common Mistakes

  • Forgetting to return after calling the callback with an error — the function continues executing and may call the callback twice

  • Not following the error-first convention — putting data as the first argument confuses everyone and breaks util.promisify()

  • Calling a callback synchronously in some code paths and asynchronously in others — this creates unpredictable behavior known as 'Zalgo'

  • Throwing errors inside callbacks instead of passing them — thrown errors crash the process because there's no try/catch wrapping the async call

  • Not realizing that callbacks in Node.js core APIs run in the next event loop tick — code after the async call runs BEFORE the callback

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Callbacks & Error-First Pattern. Login to unlock this feature.