Docs LogoDocs

Callbacks - Asynchronous Function Execution

Documentation for Callbacks - Asynchronous Function Execution.

Callbacks - Asynchronous Function Execution

What are Callbacks?

A callback is a function passed as an argument to another function, to be executed later. Callbacks are fundamental to asynchronous programming in JavaScript.

Definition: A callback function is a function that is passed as an argument to another function and is invoked after some operation has completed. The "call back" refers to the fact that the function is called back at a later point.

// Simple callback
function greet(name, callback) {
  console.log("Hello, " + name);
  callback(); // Execute the callback
}

greet("John", function () {
  console.log("Callback executed!");
});
// Output:
// Hello, John
// Callback executed!

Why Use Callbacks?

PurposeDescription
Async operationsHandle operations that take time (file I/O, network)
Event handlingRespond to user interactions
CustomizationAllow callers to customize behavior
Non-blocking codeContinue execution while waiting for results
Functional patternsEnable higher-order functions like map/filter

Synchronous vs Asynchronous Callbacks

TypeExecutionExample
SynchronousExecutes immediatelyarray.map(), array.filter()
AsynchronousExecutes latersetTimeout(), fetch(), event listeners

Synchronous Callbacks

Execute immediately during the function call - the code waits for them to complete.

// Array methods use synchronous callbacks
let numbers = [1, 2, 3, 4, 5];

console.log("Before map");

let doubled = numbers.map(function (num) {
  console.log("Processing:", num);
  return num * 2; // Executes immediately, one by one
});

console.log("After map");
console.log(doubled);

// Output (in order):
// Before map
// Processing: 1
// Processing: 2
// Processing: 3
// Processing: 4
// Processing: 5
// After map
// [2, 4, 6, 8, 10]

Asynchronous Callbacks

Execute later, after some operation completes - the code continues running.

// setTimeout - executes after delay
console.log("1 - Start");

setTimeout(function () {
  console.log("2 - Delayed message");
}, 2000);

console.log("3 - End");

// Output:
// 1 - Start
// 3 - End
// 2 - Delayed message (after 2 seconds)

// The code doesn't wait for setTimeout!

// Event listener - executes when event occurs
document.getElementById("btn").addEventListener("click", function () {
  console.log("Button clicked!"); // Runs whenever user clicks
});

// File reading (Node.js) - executes when file is read
const fs = require("fs");
fs.readFile("file.txt", "utf8", function (error, data) {
  if (error) {
    console.error("Error:", error);
  } else {
    console.log("File contents:", data);
  }
});
console.log("This runs before file is read!");

Common Callback Patterns

1. Error-First Callbacks (Node.js Convention)

The standard pattern in Node.js where the first argument is always the error.

// Pattern: callback(error, result)
function readFile(filename, callback) {
  // Simulate async operation
  setTimeout(function () {
    if (filename === "error.txt") {
      callback(new Error("File not found"), null);
    } else {
      callback(null, "File contents here");
    }
  }, 1000);
}

// Usage - always check error first!
readFile("data.txt", function (error, data) {
  if (error) {
    console.error("Error:", error.message);
    return; // Stop execution
  }
  console.log("Data:", data);
});

// Multiple error-first callbacks
function fetchUserData(userId, callback) {
  // Simulate database query
  setTimeout(() => {
    if (!userId) {
      callback(new Error("User ID required"), null);
    } else if (userId < 0) {
      callback(new Error("Invalid user ID"), null);
    } else {
      callback(null, { id: userId, name: "John", email: "john@example.com" });
    }
  }, 500);
}

2. Success/Failure Callbacks

Separate callbacks for success and failure cases.

function fetchData(url, onSuccess, onFailure) {
  setTimeout(function () {
    if (url && url.startsWith("http")) {
      onSuccess({ data: "Some data from " + url });
    } else {
      onFailure(new Error("Invalid URL"));
    }
  }, 1000);
}

// Usage
fetchData(
  "https://api.example.com",
  function (response) {
    console.log("Success:", response.data);
  },
  function (error) {
    console.error("Failure:", error.message);
  },
);

3. Callback with Context

Passing context (this value) to callbacks.

function processArray(arr, callback, context) {
  for (let i = 0; i < arr.length; i++) {
    callback.call(context, arr[i], i, arr);
  }
}

let formatter = {
  prefix: "Item:",
  format(item, index) {
    console.log(`${this.prefix} [${index}] ${item}`);
  },
};

processArray([1, 2, 3], formatter.format, formatter);
// Output:
// Item: [0] 1
// Item: [1] 2
// Item: [2] 3

4. Completion Callback Pattern

Execute callback when all operations complete.

function processItems(items, processOne, onComplete) {
  let processed = 0;

  items.forEach((item, index) => {
    processOne(item, (result) => {
      processed++;
      console.log(`Processed ${item}: ${result}`);

      if (processed === items.length) {
        onComplete(); // All done!
      }
    });
  });
}

processItems(
  [1, 2, 3],
  (item, done) => setTimeout(() => done(item * 2), Math.random() * 1000),
  () => console.log("All items processed!"),
);

Callback Hell (Pyramid of Doom)

When callbacks are nested deeply, code becomes hard to read and maintain.

// ❌ Callback hell - deeply nested, hard to maintain
getUserData(userId, function (error, user) {
  if (error) {
    handleError(error);
  } else {
    getUserPosts(user.id, function (error, posts) {
      if (error) {
        handleError(error);
      } else {
        getPostComments(posts[0].id, function (error, comments) {
          if (error) {
            handleError(error);
          } else {
            getCommentAuthor(comments[0].authorId, function (error, author) {
              if (error) {
                handleError(error);
              } else {
                console.log("Author:", author.name);
              }
            });
          }
        });
      }
    });
  }
});

// Problems:
// - Hard to read (horizontal growth)
// - Difficult to handle errors consistently
// - Hard to maintain and debug
// - Error handling duplicated everywhere
// - Difficult to reuse code

Solutions to Callback Hell

// ✅ Solution 1: Named functions (extract into separate functions)
function handleUser(error, user) {
  if (error) return handleError(error);
  getUserPosts(user.id, handlePosts);
}

function handlePosts(error, posts) {
  if (error) return handleError(error);
  getPostComments(posts[0].id, handleComments);
}

function handleComments(error, comments) {
  if (error) return handleError(error);
  getCommentAuthor(comments[0].authorId, handleAuthor);
}

function handleAuthor(error, author) {
  if (error) return handleError(error);
  console.log("Author:", author.name);
}

getUserData(userId, handleUser); // Clean entry point

// ✅ Solution 2: Promises (modern approach)
getUserData(userId)
  .then((user) => getUserPosts(user.id))
  .then((posts) => getPostComments(posts[0].id))
  .then((comments) => getCommentAuthor(comments[0].authorId))
  .then((author) => console.log("Author:", author.name))
  .catch(handleError); // Single error handler!

// ✅ Solution 3: Async/Await (best approach)
async function getAuthorInfo(userId) {
  try {
    const user = await getUserData(userId);
    const posts = await getUserPosts(user.id);
    const comments = await getPostComments(posts[0].id);
    const author = await getCommentAuthor(comments[0].authorId);
    console.log("Author:", author.name);
  } catch (error) {
    handleError(error);
  }
}

Callback Timing and Event Loop

JavaScript is single-threaded but handles async operations through the event loop.

console.log("1");

setTimeout(function () {
  console.log("2");
}, 0); // Even with 0 delay!

console.log("3");

// Output:
// 1
// 3
// 2

// Why? setTimeout is async, callback goes to the callback queue
// It only executes after the current code (synchronous) finishes

Event Loop Visualization

┌─────────────────────────────────────────────────────────┐
│                      Call Stack                          │
│  (Executes synchronous code)                            │
└─────────────────────────────────────────────────────────┘
          ↓ (async operations)        ↑ (when stack empty)
┌─────────────────────────┐    ┌────────────────────────────┐
│       Web APIs          │    │      Callback Queue        │
│  - setTimeout           │ ──→│  (Waiting callbacks)       │
│  - fetch                │    │                            │
│  - DOM events           │    │                            │
└─────────────────────────┘    └────────────────────────────┘

Event Loop: Moves callbacks from queue to stack when stack is empty
// More complex example
console.log("Start");

setTimeout(() => console.log("Timeout 1"), 0);
setTimeout(() => console.log("Timeout 2"), 0);

Promise.resolve().then(() => console.log("Promise"));

console.log("End");

// Output:
// Start
// End
// Promise        ← Microtask queue (higher priority)
// Timeout 1      ← Callback queue
// Timeout 2

Error Handling in Callbacks

Try-Catch Doesn't Work with Async Callbacks

// ❌ This doesn't work - callback runs in different execution context
try {
  setTimeout(function () {
    throw new Error("Async error");
  }, 1000);
} catch (error) {
  console.log("Caught:", error); // Never executes!
}
// Error is thrown but not caught!

// ✅ Handle errors inside the callback
setTimeout(function () {
  try {
    throw new Error("Async error");
  } catch (error) {
    console.log("Caught:", error); // Works!
  }
}, 1000);

// ✅ Error-first callback pattern (preferred)
function asyncOperation(callback) {
  setTimeout(function () {
    try {
      // Some risky operation
      const result = riskyComputation();
      callback(null, result);
    } catch (error) {
      callback(error, null);
    }
  }, 1000);
}

asyncOperation(function (error, result) {
  if (error) {
    console.error("Error:", error.message);
    return;
  }
  console.log("Result:", result);
});

Practical Callback Examples

1. Custom forEach Implementation

function customForEach(array, callback) {
  for (let i = 0; i < array.length; i++) {
    callback(array[i], i, array);
  }
}

customForEach([1, 2, 3], function (item, index) {
  console.log(`Index ${index}: ${item}`);
});

2. Retry Logic with Callbacks

function retryOperation(operation, maxRetries, delay, callback) {
  let attempts = 0;

  function attempt() {
    attempts++;
    console.log(`Attempt ${attempts}/${maxRetries}`);

    operation(function (error, result) {
      if (error) {
        if (attempts < maxRetries) {
          setTimeout(attempt, delay); // Try again after delay
        } else {
          callback(new Error(`Failed after ${maxRetries} attempts`), null);
        }
      } else {
        callback(null, result);
      }
    });
  }

  attempt();
}

// Usage: Unreliable operation
function unreliableAPI(callback) {
  const success = Math.random() > 0.7;
  setTimeout(() => {
    if (success) {
      callback(null, { data: "Success!" });
    } else {
      callback(new Error("Request failed"), null);
    }
  }, 500);
}

retryOperation(unreliableAPI, 5, 1000, function (error, result) {
  if (error) {
    console.log("All attempts failed:", error.message);
  } else {
    console.log("Success:", result);
  }
});

3. Sequential Execution

function runSequentially(tasks, finalCallback) {
  let results = [];
  let currentIndex = 0;

  function next() {
    if (currentIndex >= tasks.length) {
      finalCallback(null, results);
      return;
    }

    const task = tasks[currentIndex];
    currentIndex++;

    task(function (error, result) {
      if (error) {
        finalCallback(error, null);
        return;
      }
      results.push(result);
      next();
    });
  }

  next();
}

// Usage
const tasks = [
  (cb) => setTimeout(() => cb(null, "Task 1 done"), 1000),
  (cb) => setTimeout(() => cb(null, "Task 2 done"), 500),
  (cb) => setTimeout(() => cb(null, "Task 3 done"), 800),
];

runSequentially(tasks, function (error, results) {
  if (error) {
    console.log("Error:", error);
  } else {
    console.log("All tasks done:", results);
    // ['Task 1 done', 'Task 2 done', 'Task 3 done']
  }
});

4. Parallel Execution with Callbacks

function runParallel(tasks, finalCallback) {
  let results = new Array(tasks.length);
  let completed = 0;
  let hasError = false;

  tasks.forEach(function (task, index) {
    task(function (error, result) {
      if (hasError) return; // Ignore if already errored

      if (error) {
        hasError = true;
        finalCallback(error, null);
        return;
      }

      results[index] = result; // Preserve order
      completed++;

      if (completed === tasks.length) {
        finalCallback(null, results);
      }
    });
  });
}

// All run at the same time, results collected in order
runParallel(tasks, function (error, results) {
  console.log("All done:", results);
});

Interview Questions & Answers

Q1: What is a callback function?

Answer:

A callback function is a function passed as an argument to another function, which is then invoked inside the outer function to complete some action. The term "callback" refers to the fact that the function is "called back" at a later point.

function fetchData(url, callback) {
  // Perform async operation...
  const data = { result: "data" };
  callback(data); // "Call back" with the result
}

fetchData("/api/users", function (result) {
  console.log(result);
});

Callbacks are essential for handling asynchronous operations, events, and customizing function behavior.


Q2: What is callback hell and how do you avoid it?

Answer:

Callback hell (pyramid of doom) occurs when multiple nested callbacks make code difficult to read and maintain.

// ❌ Callback hell
op1(function (r1) {
  op2(r1, function (r2) {
    op3(r2, function (r3) {
      op4(r3, function (r4) {
        // Deep nesting...
      });
    });
  });
});

Solutions:

SolutionDescription
Named functionsExtract callbacks into separate named functions
PromisesUse .then() chaining for sequential operations
Async/AwaitWrite async code that looks synchronous
ModularizationBreak code into smaller, reusable modules

Q3: Why doesn't try-catch work with asynchronous callbacks?

Try-catch doesn't work with asynchronous callbacks because the callback executes later, after the try-catch block has already completed. When an error occurs inside an async callback, the original try-catch is no longer on the call stack. The error happens in a different execution context. To handle errors in async callbacks, you need to either use try-catch inside the callback itself, use the error-first callback pattern where errors are passed as the first argument, or use Promises which have built-in error handling with .catch().

Q4: What's the difference between synchronous and asynchronous callbacks?

Answer:

Synchronous callbacks execute immediately during the function call and block further execution until complete. Asynchronous callbacks are scheduled to execute later, allowing the program to continue running other code. Async callbacks are essential for operations like network requests, file I/O, and timers that would otherwise freeze the application.

FeatureSynchronousAsynchronous
ExecutionImmediately, in orderLater, after operation completes
Blocks codeYesNo
Examplesmap(), filter(), sort()setTimeout(), fetch(), events
Error handlingtry-catch worksNeed callback error handling
Call stackSame contextDifferent context
// Synchronous - blocks until done
[1, 2, 3].forEach((x) => console.log(x));
console.log("Done"); // After 1, 2, 3

// Asynchronous - doesn't block
setTimeout(() => console.log("Later"), 0);
console.log("Now"); // "Now" prints first!

Q5: What is the error-first callback pattern?

Answer:

A Node.js convention where the callback's first parameter is always the error (or null if no error), and subsequent parameters are the result data.

function asyncOperation(callback) {
  // callback(error, result1, result2, ...)

  if (somethingWentWrong) {
    callback(new Error("Operation failed"), null);
  } else {
    callback(null, result); // null means no error
  }
}

// Usage - always check error first
asyncOperation(function (error, data) {
  if (error) {
    console.error("Error:", error.message);
    return; // Stop here
  }
  // Safe to use data
  console.log("Data:", data);
});

Benefits:

  • Consistent error handling pattern
  • Error is always checked first
  • Clear separation of error and success cases
  • Standard across Node.js ecosystem

Q6: How do you convert callback-based code to Promises?

Answer:

Wrap the callback function in a new Promise, calling resolve() for success and reject() for errors:

// Callback-based function
function readFile(path, callback) {
  setTimeout(() => {
    if (path) callback(null, "file contents");
    else callback(new Error("Path required"), null);
  }, 100);
}

// Convert to Promise ("promisification")
function readFilePromise(path) {
  return new Promise((resolve, reject) => {
    readFile(path, (error, data) => {
      if (error) reject(error);
      else resolve(data);
    });
  });
}

// Usage
readFilePromise("/path/to/file")
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

// Node.js has util.promisify for this:
const util = require("util");
const readFilePromise = util.promisify(readFile);

Q7: Explain the event loop and how it relates to callbacks.

Answer:

The event loop is JavaScript's mechanism for handling asynchronous operations in a single-threaded environment.

1. Call Stack    - Executes synchronous code
2. Web APIs      - Handle async operations (setTimeout, fetch, etc.)
3. Callback Queue - Holds callbacks waiting to execute
4. Microtask Queue - Holds Promise callbacks (higher priority)
5. Event Loop    - Moves callbacks to stack when it's empty
console.log("1"); // Call stack
setTimeout(() => console.log("2"), 0); // Goes to Web API → Callback queue
Promise.resolve().then(() => console.log("3")); // Microtask queue
console.log("4"); // Call stack

// Output: 1, 4, 3, 2
// Microtasks run before callback queue!

Q8: What are the alternatives to callbacks for async programming?

Answer:

ApproachSyntaxError HandlingReadability
CallbacksNested functionsError-first patternPoor (callback hell)
Promises.then()/.catch().catch() chainGood
Async/Awaitasync/awaittry-catchBest
Observables (RxJS)Stream-based.catch() operatorGood for streams
// Callbacks
getData(url, (err, data) => {
  if (err) handleError(err);
  else process(data);
});

// Promises
getData(url).then(process).catch(handleError);

// Async/Await
try {
  const data = await getData(url);
  process(data);
} catch (err) {
  handleError(err);
}

Practical Examples

// Example 1: Simple animation with callbacks
function animate(element, property, start, end, duration, onComplete) {
  const startTime = Date.now();

  function step() {
    const elapsed = Date.now() - startTime;
    const progress = Math.min(elapsed / duration, 1);

    const value = start + (end - start) * progress;
    element.style[property] = value + "px";

    if (progress < 1) {
      requestAnimationFrame(step);
    } else if (onComplete) {
      onComplete();
    }
  }

  step();
}

// Animate with completion callback
// animate(div, 'left', 0, 300, 1000, () => console.log('Done!'));

// Example 2: Event emitter with callbacks
class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    return this; // For chaining
  }

  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter((cb) => cb !== callback);
    }
    return this;
  }

  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach((callback) => callback(...args));
    }
    return this;
  }
}

const emitter = new EventEmitter();
emitter
  .on("data", (data) => console.log("Received:", data))
  .on("error", (err) => console.error("Error:", err));

emitter.emit("data", { message: "Hello!" });

// Example 3: Waterfall pattern (each step uses previous result)
function waterfall(tasks, finalCallback) {
  let index = 0;

  function next(error, ...results) {
    if (error) {
      return finalCallback(error);
    }

    const task = tasks[index++];

    if (!task) {
      return finalCallback(null, ...results);
    }

    // Pass results to next task
    task(...results, next);
  }

  next(null); // Start the chain
}

waterfall(
  [
    (next) => {
      console.log("Step 1");
      next(null, 1);
    },
    (result, next) => {
      console.log("Step 2, got:", result);
      next(null, result + 1);
    },
    (result, next) => {
      console.log("Step 3, got:", result);
      next(null, result + 1);
    },
  ],
  (error, result) => {
    console.log("Final result:", result); // 3
  },
);
Last updated on July 15, 2026

On this page