Asynchronous JS
Callbacks, Promises, async/await
What is Asynchronous JavaScript?
JavaScript is single-threaded, meaning it can only do one thing at a time. But async programming allows JavaScript to handle time-consuming tasks (like fetching data from a server) without freezing the entire page.
Think of it like ordering food at a restaurant: you don't stand at the kitchen waiting for your meal (blocking). Instead, you sit down, and the waiter brings it when ready (async).
The Problem: Blocking Code
❌ Synchronous (Blocking)
✓ Asynchronous (Non-blocking)
// ❌ Blocking (DON'T DO THIS!)
console.log("Start");
const end = Date.now() + 3000;
while (Date.now() < end) {} // Freezes browser!
console.log("End");
// ✓ Non-blocking
console.log("Start");
setTimeout(() => {
console.log("After 3 seconds");
}, 3000);
console.log("End"); // Runs immediately!
Three Ways to Handle Async Code
1. Callbacks (Old Way—Avoid)
Pass a function to be called when operation completes:
setTimeout(() => {
console.log("Callback executed");
}, 1000);
// Real example: Reading a file
fs.readFile('data.txt', (error, data) => {
if (error) {
console.error(error);
} else {
console.log(data);
}
});
⚠️ Callback Hell (Pyramid of Doom)
// Nested callbacks = unreadable code
getUser(1, (user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
getLikes(comments[0].id, (likes) => {
// 😱 This keeps going...
});
});
});
});
2. Promises (ES6—Better)
Promises represent a value that will eventually be available:
Promise States:
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve("✓ Operation successful!");
} else {
reject("✗ Operation failed!");
}
}, 1000);
});
// Using a Promise
myPromise
.then(result => {
console.log(result); // "✓ Operation successful!"
return "Next step";
})
.then(nextResult => {
console.log(nextResult); // "Next step"
})
.catch(error => {
console.error(error); // Catches any error
})
.finally(() => {
console.log("Always runs");
});
💡 Chaining Promises (Better than Callbacks!)
fetch('https://api.example.com/user/1')
.then(response => response.json())
.then(user => {
console.log(user.name);
return fetch(`https://api.example.com/posts?userId=${user.id}`);
})
.then(response => response.json())
.then(posts => console.log(posts))
.catch(error => console.error("Error:", error));
3. Async/Await (ES2017—Best!) ✨
The modern, cleanest way—makes async code look synchronous:
// Async function automatically returns a Promise
async function fetchUserData() {
try {
// await pauses here until promise resolves
const response = await fetch('https://api.example.com/user/1');
const user = await response.json();
console.log(user.name);
const postsResponse = await fetch(`https://api.example.com/posts?userId=${user.id}`);
const posts = await postsResponse.json();
console.log(posts);
return posts; // Returns a Promise!
} catch (error) {
console.error("Error:", error);
throw error;
}
}
// Using async function
fetchUserData()
.then(posts => console.log("Got posts:", posts))
.catch(error => console.error("Failed:", error));
✓ Clean and Readable!
- • Looks like synchronous code
- • Easy to read and understand
- • Error handling with familiar try/catch
- • No callback hell or .then() chains
Parallel vs Sequential Execution
Understanding when to run tasks in parallel can dramatically speed up your code:
Sequential (Slower) ⏱️
async function sequential() {
const user = await getUser(); // 1s
const posts = await getPosts(); // 1s
const comments = await getComments(); // 1s
return { user, posts, comments };
}
Parallel (Faster!) ⚡
async function parallel() {
// Start all requests at once
const [user, posts, comments] = await Promise.all([
getUser(), // All three run
getPosts(), // simultaneously!
getComments()
]);
return { user, posts, comments };
}
Promise Utility Methods
Promise.all()
Wait for ALL to complete. Fails if ANY fails.
const results = await Promise.all([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/comments')
]);
// All or nothing!
Promise.race()
First one to finish wins!
const fastest = await Promise.race([
fetch('/server1/data'),
fetch('/server2/data'),
fetch('/server3/data')
]);
// Returns first response
Promise.allSettled()
Wait for all, get all results (success or failure).
const results = await Promise.allSettled([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/invalid') // Won't fail!
]);
// [{status: 'fulfilled'}, ...]
Promise.any()
First SUCCESS wins (ignores failures).
const first = await Promise.any([
fetch('/api/backup1'),
fetch('/api/backup2'),
fetch('/api/backup3')
]);
// First successful response
Common Async Patterns
1. Timeout Helper
function timeout(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function demo() {
console.log("Start");
await timeout(2000);
console.log("2 seconds later");
}
2. Retry with Exponential Backoff
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error;
// Wait longer each time: 1s, 2s, 4s
await timeout(1000 * Math.pow(2, i));
}
}
}
3. Loading State Pattern
async function loadData() {
const state = { data: null, loading: true, error: null };
try {
state.data = await fetch('/api/data').then(r => r.json());
} catch (e) {
state.error = e.message;
} finally {
state.loading = false;
}
return state;
}
// Usage in React/Vue
const { data, loading, error } = await loadData();
4. Promise with Timeout
async function fetchWithTimeout(url, timeoutMs = 5000) {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeoutMs)
);
const fetchPromise = fetch(url).then(r => r.json());
return Promise.race([fetchPromise, timeoutPromise]);
}
// Fails if takes longer than 5 seconds
const data = await fetchWithTimeout('/api/slow-endpoint', 5000);
⚠️ Common Mistakes
- ❌ Forgetting await:
const data = fetchData();gives you a Promise, not the data! - ❌ Not handling errors: Always use try/catch or .catch()
- ❌ Sequential when should be parallel: Use Promise.all() for independent operations
- ❌ Using async/await in loops: Results in sequential execution (use Promise.all with map)
- ❌ Not returning in async functions: Async functions auto-wrap in Promise, but still need explicit return
💡 Key Concepts to Remember
- • async functions always return a Promise (automatically)
- • await can only be used inside
asyncfunctions - • await pauses execution until the Promise resolves
- • Use Promise.all() for parallel operations (way faster!)
- • Always handle errors with try/catch or .catch()
- • The event loop handles async operations—JavaScript stays single-threaded
✓ Best Practices
- ✓ Prefer async/await over .then() chains for readability
- ✓ Always use try/catch with async/await to handle errors
- ✓ Use Promise.all() for independent parallel operations
- ✓ Don't forget await—use a linter to catch this!
- ✓ Handle errors appropriately—don't swallow them silently
- ✓ Add timeout logic for network requests (they can hang forever)
- ✓ Use async IIFE if you need top-level await:
(async () => { ... })()