Async JavaScript
Callbacks, Promises, async/await — handling asynchronous operations from basics to advanced patterns
Last updated on
JavaScript is single-threaded, but most real-world operations (API calls, file reads, timers) are asynchronous. Understanding async patterns is essential for every JS developer.
The Evolution of Async JS
Callbacks (1995) → Promises (ES6/2015) → async/await (ES2017)1. Callbacks
A callback is a function passed to another function, to be executed later:
function fetchData(callback) {
setTimeout(() => {
callback({ id: 1, name: "Shiva" });
}, 1000);
}
fetchData((data) => {
console.log(data.name); // "Shiva" (after 1 second)
});Callback Hell (The Problem)
Nested callbacks become unreadable:
getUser(userId, (user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
getAuthor(comments[0].authorId, (author) => {
console.log(author.name);
// Welcome to pyramid of doom 🔺
});
});
});
});2. Promises — The Solution
A Promise represents a value that will be available in the future.
Three States
Pending → Fulfilled (resolved with a value)
→ Rejected (rejected with an error)Creating a Promise
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("Data loaded!");
} else {
reject(new Error("Something went wrong"));
}
});Consuming with .then / .catch / .finally
fetch("https://api.example.com/users")
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error("Failed:", error.message);
})
.finally(() => {
console.log("Request completed"); // runs regardless
});Chaining Promises
getUser(userId)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => console.log(comments))
.catch(error => console.error(error));
// Flat chain — much better than nested callbacks!3. async / await — The Best Way
async/await is syntactic sugar over Promises. It makes async code look synchronous.
async function loadUserData(userId) {
try {
const user = await getUser(userId);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
console.log(comments);
} catch (error) {
console.error("Failed:", error.message);
}
}Key Rules
awaitcan only be used insideasyncfunctions (or at top level in ES modules)awaitpauses execution until the Promise settlesasyncfunctions always return a Promise
async function getNumber() {
return 42; // automatically wrapped in Promise.resolve(42)
}
getNumber().then(n => console.log(n)); // 42Error Handling in Async Code
try/catch with async/await
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Fetch failed:", error.message);
return null;
}
}.catch with Promises
fetch("/api/data")
.then(res => res.json())
.catch(err => {
console.error(err);
return { fallback: true };
});Promise Static Methods
Promise.all — All Must Succeed
Runs promises in parallel, resolves when ALL succeed, rejects if ANY fails:
const [users, posts, comments] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
fetch("/api/comments").then(r => r.json())
]);
// All three requests run simultaneously — much faster than sequential!Promise.allSettled — Get All Results
Waits for all to complete, never rejects:
const results = await Promise.allSettled([
fetch("/api/users"),
fetch("/api/failing-endpoint"),
fetch("/api/posts")
]);
results.forEach(result => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.log("Failed:", result.reason);
}
});Promise.race — First to Finish Wins
const result = await Promise.race([
fetch("/api/primary"),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), 5000)
)
]);
// Either the fetch succeeds or it times out after 5sPromise.any — First Success Wins
const fastest = await Promise.any([
fetch("https://cdn1.example.com/data"),
fetch("https://cdn2.example.com/data"),
fetch("https://cdn3.example.com/data")
]);
// Returns whichever CDN responds first successfullySequential vs Parallel Execution
Sequential (One at a Time)
// Each waits for the previous one — SLOW
const user = await getUser(1); // 500ms
const posts = await getPosts(1); // 500ms
const comments = await getComments(1); // 500ms
// Total: ~1500msParallel (All at Once)
// All start at the same time — FAST
const [user, posts, comments] = await Promise.all([
getUser(1), // 500ms ─┐
getPosts(1), // 500ms ─┤ all run simultaneously
getComments(1) // 500ms ─┘
]);
// Total: ~500msRule: Use Promise.all when tasks are independent. Use sequential await when each task depends on the previous one.
Converting Callbacks to Promises
// Callback-based function
function readFile(path, callback) {
// ... calls callback(err, data)
}
// Promisified version
function readFileAsync(path) {
return new Promise((resolve, reject) => {
readFile(path, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// Usage
const data = await readFileAsync("./file.txt");Node.js has a built-in utility:
const { promisify } = require("util");
const readFileAsync = promisify(fs.readFile);Real-World Pattern: Retry Logic
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (response.ok) return await response.json();
throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (i === retries - 1) throw error;
console.log(`Retry ${i + 1}/${retries}...`);
await new Promise(r => setTimeout(r, 1000 * (i + 1))); // exponential backoff
}
}
}