Docs

Module-15-Asynchronous-JavaScript

9.1 Callbacks

Introduction

A callback is a function passed as an argument to another function, to be executed later (after some operation completes). Callbacks are the foundation of asynchronous programming in JavaScript.

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│                    SYNCHRONOUS VS ASYNCHRONOUS              │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│                                                             │
│  SYNCHRONOUS                  ASYNCHRONOUS                  │
│  ────────────                 ────────────                  │
│  Task 1 ─────►                Task 1 ─────►                 │
│               │                            │                │
│  Task 2 ─────►                Task 2 ─────►  (waiting)      │
│               │                     ā—„ā”€ā”€ā”€ā”€ā”€ā”€ā”˜                │
│  Task 3 ─────►                Task 3 ─────►                 │
│                                                             │
│  Blocking                     Non-blocking                  │
│  (one at a time)              (parallel work possible)      │
│                                                             │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

What is a Callback?

// A callback is just a function passed to another function
function greet(name, callback) {
  console.log('Hello, ' + name);
  callback();
}

function sayGoodbye() {
  console.log('Goodbye!');
}

greet('Alice', sayGoodbye);
// Output:
// Hello, Alice
// Goodbye!

Synchronous Callbacks

Callbacks that execute immediately (not waiting for anything):

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

numbers.forEach(function (num) {
  console.log(num); // Executes immediately for each element
});

const doubled = numbers.map((n) => n * 2);
const filtered = numbers.filter((n) => n > 2);

Asynchronous Callbacks

Callbacks that execute later (after some operation):

// setTimeout - executes after delay
setTimeout(function () {
  console.log('This runs after 2 seconds');
}, 2000);

console.log('This runs immediately');

// Output:
// This runs immediately
// This runs after 2 seconds

The Event Loop

Understanding how JavaScript handles async operations:

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│                        EVENT LOOP                               │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│                                                                 │
│   ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”     ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”     ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”     │
│   │  Call Stack │     │  Web APIs   │     │ Task Queue   │     │
│   │             │     │             │     │              │     │
│   │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │     │ setTimeout  │     │ callback1    │     │
│   │ │ func()  │ │────►│ fetch       │────►│ callback2    │     │
│   │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │     │ DOM events  │     │ callback3    │     │
│   │             │     │             │     │              │     │
│   ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜     ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜     ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜     │
│         ā–²                                        │             │
│         │                                        │             │
│         ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜             │
│                    Event Loop                                   │
│              (moves tasks to stack                             │
│               when stack is empty)                             │
│                                                                 │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Common Async Operations

OperationDescriptionExample
setTimeoutDelay executionsetTimeout(fn, 1000)
setIntervalRepeat executionsetInterval(fn, 1000)
File I/ORead/write filesfs.readFile(path, cb)
NetworkHTTP requestsfetch, XMLHttpRequest
EventsUser interactionselement.addEventListener()
DatabaseQuery operationsdb.query(sql, cb)

Callback Patterns

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

// Error is always the first parameter
function readFile(path, callback) {
  // Simulated async operation
  setTimeout(() => {
    if (path === '') {
      callback(new Error('Path is required'), null);
    } else {
      callback(null, 'File contents here');
    }
  }, 100);
}

// Usage
readFile('data.txt', function (error, data) {
  if (error) {
    console.error('Error:', error.message);
    return;
  }
  console.log('Data:', data);
});
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│               ERROR-FIRST CALLBACK PATTERN                  │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│                                                             │
│  callback(error, result)                                    │
│            │       │                                        │
│            │       └── Success data (null if error)         │
│            │                                                │
│            └── Error object (null if success)               │
│                                                             │
│  if (error) {                                               │
│      // Handle error                                        │
│      return;                                                │
│  }                                                          │
│  // Use result                                              │
│                                                             │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

2. Success/Error Callbacks

function fetchData(url, onSuccess, onError) {
  setTimeout(() => {
    if (url.includes('error')) {
      onError(new Error('Failed to fetch'));
    } else {
      onSuccess({ data: 'Result' });
    }
  }, 100);
}

fetchData(
  '/api/users',
  (data) => console.log('Success:', data),
  (error) => console.log('Error:', error.message)
);

Callback Hell (Pyramid of Doom)

Nested callbacks become hard to read and maintain:

// āŒ Callback Hell - deeply nested, hard to follow
getUser(userId, function (error, user) {
  if (error) {
    handleError(error);
    return;
  }
  getOrders(user.id, function (error, orders) {
    if (error) {
      handleError(error);
      return;
    }
    getOrderDetails(orders[0].id, function (error, details) {
      if (error) {
        handleError(error);
        return;
      }
      getShippingInfo(details.shippingId, function (error, shipping) {
        if (error) {
          handleError(error);
          return;
        }
        // Finally do something with all the data
        displayOrder(user, orders, details, shipping);
      });
    });
  });
});
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│                     CALLBACK HELL                           │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│                                                             │
│  doStep1(function(result1) {                                │
│      doStep2(result1, function(result2) {                   │
│          doStep3(result2, function(result3) {               │
│              doStep4(result3, function(result4) {           │
│                  doStep5(result4, function(result5) {       │
│                      // Lost in nesting...                  │
│                  });                                        │
│              });                                            │
│          });                                                │
│      });                                                    │
│  });                                                        │
│                                                             │
│  Problems:                                                  │
│  • Hard to read                                             │
│  • Difficult to maintain                                    │
│  • Error handling repeated everywhere                       │
│  • Hard to add new steps                                    │
│                                                             │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Avoiding Callback Hell

1. Named Functions

// āœ… Extract callbacks into named functions
function handleUser(error, user) {
  if (error) return handleError(error);
  getOrders(user.id, handleOrders);
}

function handleOrders(error, orders) {
  if (error) return handleError(error);
  getOrderDetails(orders[0].id, handleDetails);
}

function handleDetails(error, details) {
  if (error) return handleError(error);
  displayOrder(details);
}

// Start the chain
getUser(userId, handleUser);

2. Modularization

// āœ… Create reusable modules
const userModule = {
  getWithOrders(userId, callback) {
    getUser(userId, (error, user) => {
      if (error) return callback(error);

      getOrders(user.id, (error, orders) => {
        if (error) return callback(error);
        callback(null, { user, orders });
      });
    });
  },
};

// Clean usage
userModule.getWithOrders(123, (error, data) => {
  if (error) return handleError(error);
  console.log(data.user, data.orders);
});

3. Control Flow Libraries

// Using async library (npm package)
async.waterfall(
  [
    (callback) => getUser(userId, callback),
    (user, callback) => getOrders(user.id, callback),
    (orders, callback) => getOrderDetails(orders[0].id, callback),
  ],
  (error, result) => {
    if (error) return handleError(error);
    displayOrder(result);
  }
);

Callback Gotchas

1. Callback Called Multiple Times

// āŒ Bug: callback might be called twice
function badAsync(callback) {
  doSomething((error) => {
    if (error) {
      callback(error);
      // Missing return! Continues execution
    }
    callback(null, 'success');
  });
}

// āœ… Fixed: ensure single callback
function goodAsync(callback) {
  doSomething((error) => {
    if (error) {
      return callback(error); // Return to stop
    }
    callback(null, 'success');
  });
}

2. Losing this Context

// āŒ Problem: 'this' is lost
const obj = {
  name: 'MyObject',
  load(callback) {
    setTimeout(function () {
      console.log(this.name); // undefined!
      callback();
    }, 100);
  },
};

// āœ… Solution 1: Arrow function
const obj1 = {
  name: 'MyObject',
  load(callback) {
    setTimeout(() => {
      console.log(this.name); // "MyObject"
      callback();
    }, 100);
  },
};

// āœ… Solution 2: Bind
const obj2 = {
  name: 'MyObject',
  load(callback) {
    setTimeout(
      function () {
        console.log(this.name); // "MyObject"
        callback();
      }.bind(this),
      100
    );
  },
};

3. Zalgo (Sync/Async Inconsistency)

// āŒ Bad: Sometimes sync, sometimes async (Zalgo!)
function maybeAsync(value, callback) {
  if (cache[value]) {
    callback(cache[value]); // Sync!
  } else {
    fetchValue(value, callback); // Async!
  }
}

// āœ… Good: Always async
function alwaysAsync(value, callback) {
  if (cache[value]) {
    setTimeout(() => callback(cache[value]), 0); // Force async
  } else {
    fetchValue(value, callback);
  }
}

// āœ… Better: Use setImmediate or process.nextTick (Node.js)
function alwaysAsyncNode(value, callback) {
  if (cache[value]) {
    process.nextTick(() => callback(cache[value]));
  } else {
    fetchValue(value, callback);
  }
}

Common Callback-Based APIs

setTimeout / setInterval

// One-time delay
const timeoutId = setTimeout(() => {
  console.log('Delayed!');
}, 1000);

// Cancel before execution
clearTimeout(timeoutId);

// Repeated execution
const intervalId = setInterval(() => {
  console.log('Repeating!');
}, 1000);

// Stop the interval
clearInterval(intervalId);

Event Listeners

// DOM events
button.addEventListener('click', function (event) {
  console.log('Button clicked!', event);
});

// Remove listener
function handleClick(event) {
  console.log('Clicked!');
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);

Node.js File System

const fs = require('fs');

// Read file (async)
fs.readFile('data.txt', 'utf8', (error, data) => {
  if (error) {
    console.error('Failed to read:', error);
    return;
  }
  console.log(data);
});

// Write file (async)
fs.writeFile('output.txt', 'Hello!', (error) => {
  if (error) {
    console.error('Failed to write:', error);
    return;
  }
  console.log('File written!');
});

Callback Utilities

Debounce

// Execute only after delay since last call
function debounce(func, delay) {
  let timeoutId;

  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// Usage: only fire after user stops typing
const handleInput = debounce((value) => {
  console.log('Searching for:', value);
}, 300);

Throttle

// Execute at most once per interval
function throttle(func, interval) {
  let lastTime = 0;

  return function (...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      func.apply(this, args);
    }
  };
}

// Usage: limit scroll handler
const handleScroll = throttle(() => {
  console.log('Scroll position:', window.scrollY);
}, 100);

Summary

ConceptDescription
CallbackFunction passed as argument, called later
Sync CallbackExecutes immediately (forEach, map)
Async CallbackExecutes after operation (setTimeout, fetch)
Error-FirstConvention: callback(error, result)
Callback HellDeep nesting problem
SolutionsNamed functions, modularization, Promises

What's Next?

Callbacks work but have limitations. In the next section, we'll learn about Promises - a more elegant way to handle async operations with better:

  • •Error handling
  • •Chaining
  • •Composition
  • •Readability
// Preview: Promises solve callback hell
getUser(userId)
  .then((user) => getOrders(user.id))
  .then((orders) => getOrderDetails(orders[0].id))
  .then((details) => displayOrder(details))
  .catch((error) => handleError(error));
Module 15 Asynchronous JavaScript - JavaScript Tutorial | DeepML