What it does: A function passed to another function to be executed when an async operation completes.

Callback Pattern Structure:

Basic Callback:

function doAsync(data, callback) {
  // do something async
  setTimeout(() => {
    callback("result");
  }, 1000);
}
 
doAsync("input", function(result) {
  console.log(result); // runs after 1 second
});

Error-First Callback (Node.js style):

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  
  script.onload = () => callback(null, script);    // success
  script.onerror = () => callback(new Error());    // error
  
  document.head.append(script);
}
 
loadScript('script.js', function(error, script) {
  if (error) {
    console.log("Failed to load");
  } else {
    console.log("Script loaded!");
  }
});

The Problem - Callback Hell:

// This gets ugly fast
loadScript('1.js', function(error, script) {
  if (error) handleError(error);
  else {
    loadScript('2.js', function(error, script) {
      if (error) handleError(error);
      else {
        loadScript('3.js', function(error, script) {
          // pyramid of doom!
        });
      }
    });
  }
});

Interview Gotchas:

javascript

// 1. Callbacks don't wait
loadScript('script.js');
newFunction(); // ERROR! Script not loaded yet
 
// 2. Multiple callbacks with same function
function process(callback) {
  callback();
  callback(); // Might run twice!
}
 
// 3. Forgetting error handling
doAsync(data, function(result) {
  // What if there was an error? 💥
  console.log(result.length);
});

Bottom line: Callbacks let you handle async operations, but they create nested “pyramid of doom” code that’s hard to read and maintain.