Asynchronous programming is essential for non-blocking operations like fetching data, reading files, or waiting for user input. JavaScript handles asynchrony through callbacks, promises, and async/await.
Synchronous code runs line by line, blocking further execution until each operation completes. Asynchronous code lets the program continue while waiting for slow operations:
// Synchronous — blocks until complete
console.log("Start");
const data = fetchDataSync(); // Blocks for 2 seconds
console.log(data);
// Asynchronous — does not block
console.log("Start");
fetchDataAsync((data) => {
console.log(data); // Runs later
});
console.log("End"); // Runs immediately after "Start"
A callback is a function passed as an argument to another function, to be executed later:
function fetchUser(id, callback) {
setTimeout(() => {
callback({ id, name: "Alice" });
}, 1000);
}
fetchUser(1, (user) => {
console.log("User:", user.name);
});
console.log("Waiting for user...");
Nesting callbacks creates deeply indented, hard-to-maintain code known as "callback hell":
fetchUser(1, (user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
fetchLikes(comments[0].id, (likes) => {
console.log(likes);
// Deep nesting continues...
});
});
});
});
A Promise represents a value that may be available now, later, or never. It has three states: pending, fulfilled, or rejected.
const promise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
resolve("Operation succeeded!");
} else {
reject("Operation failed!");
}
}, 1000);
});
promise
.then((result) => console.log(result))
.catch((error) => console.error(error))
.finally(() => console.log("Done"));
fetchUser(1)
.then((user) => fetchPosts(user.id))
.then((posts) => fetchComments(posts[0].id))
.then((comments) => console.log(comments))
.catch((error) => console.error("Error:", error));
Wait for all promises to resolve (or any to reject):
const p1 = fetchUser(1);
const p2 = fetchUser(2);
const p3 = fetchUser(3);
Promise.all([p1, p2, p3])
.then((users) => console.log("All users:", users))
.catch((err) => console.error("One failed:", err));
Resolve or reject as soon as the first promise settles:
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), 5000)
);
const data = fetch("https://api.example.com/data");
Promise.race([data, timeout])
.then((result) => console.log(result))
.catch((err) => console.error(err));
Promise.allSettled waits for all promises regardless of rejection, returning their outcomes. Promise.any resolves when the first promise fulfills (rejects only if all reject).
Introduced in ES2017, async/await makes asynchronous code look synchronous:
async function loadUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return comments;
} catch (error) {
console.error("Failed to load user data:", error);
throw error;
}
}
// Usage
loadUserData(1)
.then((comments) => console.log(comments))
.catch((err) => console.error(err));
async function greet(name) {
return `Hello, ${name}!`;
}
greet("Alice").then(console.log); // "Hello, Alice!"
async function example() {
// Parallel execution with await
const [user1, user2] = await Promise.all([
fetchUser(1),
fetchUser(2)
]);
console.log(user1, user2);
}
Always wrap await calls in try/catch blocks to handle rejections gracefully:
async function safeFetch(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Fetch failed for ${url}:`, error.message);
return null; // Graceful fallback
}
}
The event loop is JavaScript's concurrency model. It continuously checks the call stack and callback queue:
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve()
.then(() => console.log("3"));
console.log("4");
// Output: 1, 4, 3, 2
// Microtask (Promise) runs before the next task (setTimeout)
queueMicrotask) execute before the next macrotask (setTimeout, setInterval, I/O). This means promises resolve before timers, even when the timer delay is 0ms.
setTimeout wrapped in a Promise to simulate each async operation. Also add a timeout race using Promise.race that rejects if the whole chain takes longer than 4 seconds.