Async JavaScript
Callbacks, promises, async/await, and fetching data from APIs without blocking.
What you'll learn
- Why JavaScript is single-threaded and what the event loop does
- The problem with callbacks (callback hell)
- Promises: then, catch, finally, and chaining
- async/await for linear, readable async code
- Fetching JSON from an API with fetch()
Concept
The Event Loop
JavaScript is single-threaded — it runs one task at a time. To avoid freezing the page on slow operations, the event loop pushes long-running work (network requests, timers) to the browser's background APIs and runs the callback when the result is ready.
This means async code does not run top-to-bottom. Understanding this is the key to avoiding race conditions and bugs.
Callbacks (the old way)
A callback is a function passed to be called later:
setTimeout(() => {
console.log("runs after 1 second");
}, 1000);
Nesting many callbacks creates callback hell — deeply indented, hard-to-read code:
getUser(id, (user) => {
getOrders(user, (orders) => {
getOrderItems(orders[0], (items) => {
// ... keeps nesting
});
});
});
Promises
A promise represents a value that will be available in the future. It can be pending, fulfilled, or rejected.
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error("Failed:", error))
.finally(() => console.log("Done"));
Promises chain flatly and propagate errors to the next catch, which is much cleaner than nested callbacks.
async / await
async/await (ES2017) lets you write async code that looks synchronous. An async function always returns a promise, and await pauses until the promise resolves:
async function loadData() {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Failed:", error);
}
}
This is the modern standard. Use it instead of then chains whenever possible.
Fetching Data
fetch() is the built-in API for making HTTP requests. It returns a promise that resolves to a Response object:
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Ada" }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const users = await res.json();
Always check res.ok — fetch only rejects on network errors, not on HTTP 404 or 500.
Running Promises in Parallel
// Sequential — slow
const a = await fetchA();
const b = await fetchB();
// Parallel — faster
const [aResult, bResult] = await Promise.all([fetchA(), fetchB()]);
Promise.all runs promises concurrently and waits for all of them. Use it to speed up independent requests.
Example
// async/await with fetch and error handling
async function getUser(userId) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error("Failed to fetch user:", error.message);
return null;
}
}
// Run multiple requests in parallel
async function loadDashboard() {
const [user1, user2, user3] = await Promise.all([
getUser(1),
getUser(2),
getUser(3),
]);
const users = [user1, user2, user3].filter(Boolean);
users.forEach((u) => console.log(`${u.name} — ${u.email}`));
}
// A simple promise you can create yourself
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function countdown() {
for (let i = 3; i > 0; i--) {
console.log(i);
await delay(1000);
}
console.log("Go!");
}
loadDashboard();
countdown();
Shows a real fetch call with error handling, Promise.all for parallel requests, creating a promise from scratch (delay), and using await with a timer to build a countdown.
Try it
- HTTP Request Tester
Send GET/POST requests and inspect responses — exactly what fetch does.
- WebSocket Client Tester
Async communication over WebSockets — another async pattern.
Common mistakes
The mistake
Forgetting await and getting a Promise object instead of the value
The fix
await pauses until the promise resolves. Without it, you get the Promise object itself. Always use await inside async functions when you need the resolved value.
The mistake
Not checking response.ok after fetch
The fix
fetch only rejects on network errors. HTTP 404 or 500 still resolve. Always check if (!response.ok) throw new Error(...) before calling response.json().
Related Resources
Related Tools
- Server-Sent Events Tester
Another async streaming pattern used in modern web apps.
- HTTP Status Codes
Reference for the status codes your fetch calls will return.
Related Cheatsheets
- JavaScript Cheatsheet
Promise, async/await, and fetch reference.
Practice Async JavaScript
2 exercises
Look up unfamiliar terms
A beginner-friendly glossary explains jargon in plain English — variables, functions, DOM, Promise, and more.
Build real projects
Apply what you learned by building guided projects with starter code and solutions.
Decode error messages
Plain-English explanations of common errors — what they mean, why they happen, and how to fix them.