Module 16: Asynchronous JavaScript - Callbacks
JavaScript is single-threaded but handles asynchronous operations through the event loop. Understanding callbacks is the first step to mastering async programming.
1. Understanding Synchronous vs Asynchronous
1.1 Synchronous Code (Blocking)
console.log('Start');
// Blocking operation
function heavyCalculation() {
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += i;
}
return sum;
}
const result = heavyCalculation(); // Blocks execution
console.log('Result:', result);
console.log('End');
// Output (in order):
// Start
// (wait...)
// Result: ...
// End
1.2 Asynchronous Code (Non-blocking)
console.log('Start');
setTimeout(() => {
console.log('Async operation');
}, 1000);
console.log('End');
// Output:
// Start
// End
// (after 1 second)
// Async operation
Single Thread
JavaScript runs on a single thread, but async operations don't block the thread. They're handled by browser APIs and return via the event loop.
2. What are Callbacks?
A callback is a function passed as an argument to another function and executed after some operation completes.
2.1 Synchronous Callbacks
// Array methods use callbacks
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function(num) {
console.log(num);
});
const doubled = numbers.map(function(num) {
return num * 2;
});
// Custom synchronous callback
function greet(name, callback) {
console.log('Hello, ' + name);
callback();
}
greet('John', function() {
console.log('Callback executed');
});
2.2 Asynchronous Callbacks
// setTimeout
setTimeout(function() {
console.log('Executed after 1 second');
}, 1000);
// Event listeners
button.addEventListener('click', function() {
console.log('Button clicked');
});
// AJAX with callbacks (old style)
function fetchData(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() {
if (xhr.status === 200) {
callback(null, JSON.parse(xhr.responseText));
} else {
callback(new Error('Request failed'));
}
};
xhr.send();
}
3. Common Async Patterns
3.1 setTimeout
// Execute after delay
setTimeout(function() {
console.log('Delayed execution');
}, 2000);
// With arrow function
setTimeout(() => {
console.log('Arrow function callback');
}, 1000);
// Store timer ID
const timerId = setTimeout(() => {
console.log('This will be cancelled');
}, 3000);
// Cancel timeout
clearTimeout(timerId);
3.2 setInterval
// Execute repeatedly
const intervalId = setInterval(function() {
console.log('Executing every second');
}, 1000);
// Stop after 5 seconds
setTimeout(() => {
clearInterval(intervalId);
console.log('Interval stopped');
}, 5000);
// Countdown example
let count = 10;
const countdownId = setInterval(() => {
console.log(count);
count--;
if (count < 0) {
clearInterval(countdownId);
console.log('Done!');
}
}, 1000);
3.3 Event Listeners
const button = document.querySelector('#myButton');
// Add callback
button.addEventListener('click', function(event) {
console.log('Clicked at:', event.clientX, event.clientY);
});
// Named callback (can be removed)
function handleClick(event) {
console.log('Button clicked');
}
button.addEventListener('click', handleClick);
// Remove callback
button.removeEventListener('click', handleClick);
3.4 File Reading (Node.js)
const fs = require('fs');
// Async file read with callback
fs.readFile('data.txt', 'utf8', function(error, data) {
if (error) {
console.error('Error reading file:', error);
return;
}
console.log('File content:', data);
});
console.log('This runs before file is read');
4. Error-First Callbacks (Node.js Convention)
4.1 Pattern
function doSomethingAsync(param, callback) {
setTimeout(() => {
if (param === 'error') {
callback(new Error('Something went wrong'), null);
} else {
callback(null, 'Success: ' + param);
}
}, 1000);
}
// Usage
doSomethingAsync('test', function(error, result) {
if (error) {
console.error('Error:', error.message);
return;
}
console.log('Result:', result);
});
4.2 Real Example: Database Query
function queryDatabase(query, callback) {
// Simulate database query
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
callback(null, [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]);
} else {
callback(new Error('Database connection failed'), null);
}
}, 1000);
}
// Usage
queryDatabase('SELECT * FROM users', function(error, results) {
if (error) {
console.error('Query failed:', error.message);
return;
}
console.log('Users:', results);
});
5. Callback Hell (Pyramid of Doom)
5.1 The Problem
// Nested callbacks become unreadable
getUserData(userId, function(error, user) {
if (error) {
console.error(error);
return;
}
getOrders(user.id, function(error, orders) {
if (error) {
console.error(error);
return;
}
getOrderDetails(orders[0].id, function(error, details) {
if (error) {
console.error(error);
return;
}
processPayment(details.total, function(error, payment) {
if (error) {
console.error(error);
return;
}
sendConfirmation(user.email, payment, function(error, sent) {
if (error) {
console.error(error);
return;
}
console.log('Order completed!');
});
});
});
});
});
5.2 Solutions
1. Named Functions
function handleUser(error, user) {
if (error) return console.error(error);
getOrders(user.id, handleOrders);
}
function handleOrders(error, orders) {
if (error) return console.error(error);
getOrderDetails(orders[0].id, handleDetails);
}
function handleDetails(error, details) {
if (error) return console.error(error);
processPayment(details.total, handlePayment);
}
function handlePayment(error, payment) {
if (error) return console.error(error);
console.log('Payment successful:', payment);
}
getUserData(userId, handleUser);
2. Modularization
function processOrder(userId) {
getUserData(userId, function(error, user) {
if (error) return handleError(error);
fetchOrderData(user, function(error, orderData) {
if (error) return handleError(error);
completeOrder(orderData);
});
});
}
function fetchOrderData(user, callback) {
getOrders(user.id, function(error, orders) {
if (error) return callback(error);
getOrderDetails(orders[0].id, callback);
});
}
function completeOrder(details) {
console.log('Order completed:', details);
}
function handleError(error) {
console.error('Error:', error);
}
Callback Hell
Deeply nested callbacks are hard to read, debug, and maintain. This led to the development of Promises and async/await.
6. Creating Custom Async Functions
6.1 Simple Async Function
function delay(ms, callback) {
setTimeout(callback, ms);
}
// Usage
delay(1000, function() {
console.log('Executed after 1 second');
});
6.2 Async Function with Parameters
function fetchUser(id, callback) {
setTimeout(() => {
const users = {
1: { id: 1, name: 'John' },
2: { id: 2, name: 'Jane' }
};
const user = users[id];
if (user) {
callback(null, user);
} else {
callback(new Error('User not found'), null);
}
}, 1000);
}
// Usage
fetchUser(1, function(error, user) {
if (error) {
console.error(error.message);
} else {
console.log('User:', user);
}
});
6.3 Multiple Callbacks
function processData(data, onSuccess, onError) {
setTimeout(() => {
if (!data) {
onError(new Error('No data provided'));
} else {
onSuccess(data.toUpperCase());
}
}, 1000);
}
// Usage
processData(
'hello',
function(result) {
console.log('Success:', result);
},
function(error) {
console.error('Error:', error.message);
}
);
7. Practical Examples
7.1 Animation with Callbacks
function animate(element, property, from, to, duration, callback) {
const start = Date.now();
const timer = setInterval(() => {
const elapsed = Date.now() - start;
const progress = Math.min(elapsed / duration, 1);
const value = from + (to - from) * progress;
element.style[property] = value + 'px';
if (progress === 1) {
clearInterval(timer);
if (callback) callback();
}
}, 16); // ~60fps
}
// Usage
const box = document.querySelector('.box');
animate(box, 'left', 0, 200, 1000, function() {
console.log('Animation complete');
animate(box, 'top', 0, 200, 1000, function() {
console.log('Second animation complete');
});
});
7.2 Sequential Execution
function runSequence(tasks, finalCallback) {
let index = 0;
function runNext() {
if (index >= tasks.length) {
if (finalCallback) finalCallback();
return;
}
const task = tasks[index++];
task(function() {
runNext();
});
}
runNext();
}
// Usage
const tasks = [
function(callback) {
setTimeout(() => {
console.log('Task 1 complete');
callback();
}, 1000);
},
function(callback) {
setTimeout(() => {
console.log('Task 2 complete');
callback();
}, 500);
},
function(callback) {
setTimeout(() => {
console.log('Task 3 complete');
callback();
}, 800);
}
];
runSequence(tasks, function() {
console.log('All tasks complete');
});
7.3 Retry Logic
function retry(fn, maxAttempts, delay, callback) {
let attempts = 0;
function attempt() {
attempts++;
fn(function(error, result) {
if (error && attempts < maxAttempts) {
console.log(`Attempt ${attempts} failed, retrying...`);
setTimeout(attempt, delay);
} else if (error) {
callback(error, null);
} else {
callback(null, result);
}
});
}
attempt();
}
// Usage
function unreliableOperation(callback) {
const success = Math.random() > 0.7;
setTimeout(() => {
if (success) {
callback(null, 'Success!');
} else {
callback(new Error('Operation failed'));
}
}, 500);
}
retry(unreliableOperation, 3, 1000, function(error, result) {
if (error) {
console.error('All attempts failed:', error.message);
} else {
console.log('Operation succeeded:', result);
}
});
7.4 Parallel Execution
function parallel(tasks, callback) {
let completed = 0;
const results = [];
let hasError = false;
if (tasks.length === 0) {
callback(null, results);
return;
}
tasks.forEach((task, index) => {
task(function(error, result) {
if (hasError) return;
if (error) {
hasError = true;
callback(error, null);
return;
}
results[index] = result;
completed++;
if (completed === tasks.length) {
callback(null, results);
}
});
});
}
// Usage
const parallelTasks = [
function(callback) {
setTimeout(() => callback(null, 'Result 1'), 1000);
},
function(callback) {
setTimeout(() => callback(null, 'Result 2'), 500);
},
function(callback) {
setTimeout(() => callback(null, 'Result 3'), 800);
}
];
parallel(parallelTasks, function(error, results) {
if (error) {
console.error('Error:', error);
} else {
console.log('All results:', results);
}
});
8. The Event Loop
8.1 How It Works
console.log('1: Synchronous');
setTimeout(() => {
console.log('2: setTimeout (Macrotask)');
}, 0);
Promise.resolve().then(() => {
console.log('3: Promise (Microtask)');
});
console.log('4: Synchronous');
// Output:
// 1: Synchronous
// 4: Synchronous
// 3: Promise (Microtask)
// 2: setTimeout (Macrotask)
Call Stack, Task Queue, Microtask Queue
- Call Stack – Executes synchronous code
- Microtask Queue – Promises, queueMicrotask
- Macrotask Queue – setTimeout, setInterval, I/O
8.2 Visualization
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
Promise.resolve()
.then(() => console.log('Promise 1'))
.then(() => console.log('Promise 2'));
setTimeout(() => {
console.log('Timeout 2');
}, 0);
console.log('End');
// Execution order:
// 1. Call Stack: Start, End
// 2. Microtasks: Promise 1, Promise 2
// 3. Macrotasks: Timeout 1, Timeout 2
9. Best Practices
9.1 Always Handle Errors
// ❌ Bad
doSomethingAsync(data, function(error, result) {
console.log(result); // Crash if error!
});
// ✅ Good
doSomethingAsync(data, function(error, result) {
if (error) {
console.error('Error:', error);
return;
}
console.log('Result:', result);
});
9.2 Avoid Callback Hell
// ❌ Bad: Deep nesting
getData(function(data) {
processData(data, function(processed) {
saveData(processed, function(saved) {
// ...
});
});
});
// ✅ Good: Use named functions or Promises
9.3 Use Meaningful Names
// ❌ Bad
doAsync('data', function(e, r) {
// What are e and r?
});
// ✅ Good
doAsync('data', function(error, result) {
if (error) {
handleError(error);
} else {
processResult(result);
}
});
10. Transition to Promises
10.1 Callback to Promise Wrapper
// Callback-based function
function fetchDataCallback(url, callback) {
setTimeout(() => {
callback(null, { data: 'result' });
}, 1000);
}
// Promisify
function fetchDataPromise(url) {
return new Promise((resolve, reject) => {
fetchDataCallback(url, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
// Usage
fetchDataPromise('https://api.example.com')
.then(result => console.log(result))
.catch(error => console.error(error));
Summary
In this module, you learned:
- ✅ Difference between synchronous and asynchronous code
- ✅ What callbacks are and how they work
- ✅ Common async patterns: setTimeout, setInterval, events
- ✅ Error-first callback convention
- ✅ Callback hell and how to avoid it
- ✅ Creating custom async functions
- ✅ The event loop and task queues
- ✅ Best practices for working with callbacks
- ✅ Transitioning from callbacks to Promises
Next Steps
In Module 17, you'll learn about Promises, a more elegant solution to async programming that solves callback hell.
Practice Exercises
- Create a traffic light simulation using setTimeout callbacks
- Build a retry mechanism for failed operations
- Implement a queue that processes tasks sequentially
- Create an animation library using callbacks
- Build a simple polling mechanism
- Implement a timeout wrapper for async functions
- Create a waterfall pattern for sequential async tasks
- Build a rate limiter using callbacks