JavaScript is single-threaded, meaning it can only execute one task at a time. But thanks to its asynchronous nature, it can handle multiple operations concurrently without blocking the main thread. Let's explore how this works.
π₯ The Problem: Blocking Code
Without async operations, JavaScript would freeze while waiting for slow tasks:
// Synchronous - blocks execution
console.log('Start');
const result = fetchDataFromServer(); // Takes 3 seconds
console.log(result); // Wait... wait... wait...
console.log('End'); // Only runs after fetch completes
β‘ The Solution: Event Loop
JavaScript uses an event loop to handle async operations. Here's a visual representation:
π Callbacks: The Foundation
The oldest pattern for handling async operations:
function fetchData(callback) {
setTimeout(() => {
const data = { id: 1, name: 'Alice' };
callback(data);
}, 1000);
}
fetchData((result) => {
console.log(result); // { id: 1, name: 'Alice' }
});
getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
getMoreData(c, function(d) {
// π± Deep nesting!
});
});
});
});
π― Promises: A Better Way
Promises represent a value that may not exist yet but will be resolved at some point:
const fetchUser = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve({ id: 1, name: 'Bob' });
} else {
reject(new Error('Failed to fetch'));
}
}, 1000);
});
// Using the promise
fetchUser
.then(user => console.log(user))
.catch(error => console.error(error))
.finally(() => console.log('Done'));
β¨ Async/Await: Modern Syntax
Syntactic sugar over promises that makes async code look synchronous:
// Function marked as async
async function getUserData() {
try {
const response = await fetch('/api/user');
const user = await response.json();
console.log(user);
return user;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// Calling async function
getUserData().then(user => {
console.log('Got user:', user);
});
asyncmakes a function return a Promiseawaitpauses execution until Promise resolves- Use
try/catchfor error handling - Can only use
awaitinsideasyncfunctions
π Promise.all: Parallel Execution
Run multiple async operations concurrently:
async function loadDashboard() {
// These run in parallel!
const [user, posts, notifications] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/notifications').then(r => r.json())
]);
return { user, posts, notifications };
}
// vs sequential (slower):
async function loadDashboardSlow() {
const user = await fetch('/api/user').then(r => r.json()); // 100ms
const posts = await fetch('/api/posts').then(r => r.json()); // 100ms
const notifications = await fetch('/api/notifications')...; // 100ms
// Total: 300ms
}
β οΈ Common Pitfalls
1. Forgetting await
async function getData() {
const promise = fetch('/api/data'); // Returns Promise
console.log(promise); // Promise { }
const response = await promise; // β
Correct
const data = await response.json();
}
2. Looping with async
// β Wrong: forEach doesn't wait
async function processItems(items) {
items.forEach(async (item) => {
await saveToDatabase(item); // These run concurrently!
});
console.log('Done'); // Prints before saves complete
}
// β
Correct: Use for...of
async function processItems(items) {
for (const item of items) {
await saveToDatabase(item); // Waits for each one
}
console.log('Done'); // Prints after all saves
}
3. Unhandled Promise Rejections
// β Missing error handling
fetch('/api/data')
.then(r => r.json())
.then(data => console.log(data));
// If fetch fails, unhandled rejection!
// β
Always handle errors
fetch('/api/data')
.then(r => r.json())
.then(data => console.log(data))
.catch(err => console.error('Failed:', err));
π Quick Reference
| Pattern | Use When |
|---|---|
| Callbacks | Legacy code, simple one-time operations |
| Promises | Chaining, multiple async operations |
| Async/Await | Modern code, readable async flow |
| Promise.all() | Running multiple operations in parallel |
π Practice Exercise
Challenge: Rewrite this callback-based code using async/await:
function getUser(callback) {
setTimeout(() => callback({ id: 1 }), 100);
}
function getPosts(userId, callback) {
setTimeout(() => callback(['Post 1', 'Post 2']), 100);
}
getUser((user) => {
getPosts(user.id, (posts) => {
console.log(posts);
});
});
Solution: Check the console for hints, or visit our cheatsheet.