Asynchronous JavaScript
Why async matters
JavaScript is single-threaded — it runs one thing at a time. But web apps need to do things that take time: fetching data from a server, reading files, waiting for user input. If JavaScript stopped and waited for each operation, your page would freeze.
Asynchronous programming lets JavaScript start a slow task, continue running other code, and come back when the task finishes.
Callbacks — the original approach
JavaScript
function fetchData(callback) {
setTimeout(() => {
callback({ name: 'Alice' });
}, 1000);
}
fetchData((data) => {
console.log(data.name); // "Alice" (after 1 second)
});This works, but nesting callbacks creates "callback hell":
JavaScript
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
// deeply nested, hard to read and debug
});
});
});Promises — a cleaner pattern
A Promise represents a value that isn't available yet but will be eventually:
JavaScript
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ name: 'Alice' });
}, 1000);
});
promise
.then((data) => {
console.log(data.name); // "Alice"
})
.catch((error) => {
console.error('Something went wrong:', error);
});Promise states
- Pending — the operation is in progress.
- Fulfilled — the operation succeeded (
.then()runs). - Rejected — the operation failed (
.catch()runs).
Chaining promises
JavaScript
fetch('/api/user')
.then(response => response.json())
.then(user => fetch(`/api/orders/${user.id}`))
.then(response => response.json())
.then(orders => console.log(orders))
.catch(error => console.error(error));Each
.then() returns a new Promise, so you can chain instead of nesting.async/await — the modern way
async/await is syntactic sugar over Promises. It lets you write asynchronous code that looks synchronous:JavaScript
async function loadUser() {
try {
const response = await fetch('/api/user');
const user = await response.json();
const ordersResponse = await fetch(`/api/orders/${user.id}`);
const orders = await ordersResponse.json();
console.log(orders);
} catch (error) {
console.error('Failed to load:', error);
}
}
loadUser();await pauses execution of the function (not the whole program) until the Promise resolves. The code reads top-to-bottom, like synchronous code.Rules of async/await
awaitcan only be used inside anasyncfunction (or at the top level of a module).- An
asyncfunction always returns a Promise. - Always wrap
awaitcalls intry/catchfor error handling.
Running promises in parallel
If two operations don't depend on each other, run them simultaneously:
JavaScript
async function loadDashboard() {
const [user, notifications] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/notifications').then(r => r.json()),
]);
console.log(user, notifications);
}Promise.all() runs all promises at the same time and waits for all to finish. If any one fails, the whole thing fails.Promise.allSettled
If you want to know which succeeded and which failed:
JavaScript
const results = await Promise.allSettled([
fetch('/api/a').then(r => r.json()),
fetch('/api/b').then(r => r.json()),
]);
results.forEach((result) => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.log('Failed:', result.reason);
}
});The Fetch API
fetch is the modern way to make HTTP requests:JavaScript
// GET request
const response = await fetch('/api/posts');
const posts = await response.json();
// POST request
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Hello', content: 'World' }),
});
// Check for errors — fetch doesn't throw on 404 or 500
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}Important:
fetch only throws on network errors (like no internet). A 404 or 500 response is still a successful fetch — you must check response.ok yourself.Common patterns
Loading state
JavaScript
async function loadPosts() {
setLoading(true);
try {
const response = await fetch('/api/posts');
if (!response.ok) throw new Error('Failed to load');
const posts = await response.json();
setPosts(posts);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
}Retry logic
JavaScript
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();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
}Key takeaway
Async programming is essential for web development. Use
async/await for readability, Promise.all for parallel operations, try/catch for error handling, and always check response.ok with fetch. Understanding the event loop and how JavaScript handles asynchronous code is what separates beginners from intermediate developers.