Build reliable, non-blocking JavaScript code with callbacks, Promises, and async/await.
| Concept | Syntax / Pattern | Why It Matters |
|---|---|---|
| Callback | runTask((err, result) => {}) |
Legacy async pattern and event APIs |
| Promise | new Promise((resolve, reject) => {}) |
Standard async contract with chaining |
| Async Function | async function load() {} |
Always returns a Promise |
| Await | const data = await fetch(url) |
Write async logic in readable steps |
| Parallel Work | await Promise.all([a(), b()]) |
Run independent tasks concurrently |
| Resilient Parallel | await Promise.allSettled([...]) |
Collect success/failure without short-circuit |
| Timeout + Cancel | AbortController + setTimeout |
Prevent hanging requests and stale updates |
| Async Iteration | for await (const chunk of stream) |
Process streamed or chunked data |
- How JavaScript Concurrency Actually Works
- Callbacks
- Promises
- Async / Await
- Concurrency Patterns You Will Use Daily
- Cancellation, Timeouts, and Retries
- UI Patterns for Async Work
- Common Pitfalls and Debugging
- Practice Drills
- Production Checklist
JavaScript runs your code on a single main thread, but the runtime (browser or Node.js) can handle I/O in the background.
Core pieces:
- Call Stack: where synchronous functions execute.
- Web APIs / Runtime APIs: timers, network, file and event handling.
- Task Queue (Macrotasks):
setTimeout, DOM events, I/O callbacks. - Microtask Queue: Promise reactions (
.then,awaitcontinuations),queueMicrotask. - Event Loop: picks the next task once the call stack is empty, and drains microtasks before the next task.
console.log("A");
setTimeout(() => console.log("B: timeout"), 0);
Promise.resolve().then(() => console.log("C: microtask"));
console.log("D");
// Output:
// A
// D
// C: microtask
// B: timeoutWhy: microtasks run before moving to the next macrotask.
A callback is a function passed to be called later.
function readUser(id, callback) {
setTimeout(() => {
if (!id) {
callback(new Error("Missing id"));
return;
}
callback(null, { id, name: "Ari" });
}, 300);
}
readUser("42", (err, user) => {
if (err) {
console.error(err.message);
return;
}
console.log(user.name);
});getUser(userId, (err, user) => {
if (err) return handleError(err);
getOrders(user.id, (err2, orders) => {
if (err2) return handleError(err2);
getRecommendations(orders, (err3, recs) => {
if (err3) return handleError(err3);
render(recs);
});
});
});Typical fix: wrap callback APIs in Promises and compose with async/await.
A Promise has three states:
- pending
- fulfilled (resolved)
- rejected
function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
wait(500)
.then(() => "done")
.then((message) => console.log(message))
.catch((error) => console.error("Unexpected:", error))
.finally(() => console.log("cleanup"));function readUserPromise(id) {
return new Promise((resolve, reject) => {
readUser(id, (err, user) => {
if (err) reject(err);
else resolve(user);
});
});
}Promise.all(iterable): fail fast, best when all results are required.Promise.allSettled(iterable): never throws due to individual failures.Promise.race(iterable): first settled wins (success or failure).Promise.any(iterable): first fulfilled wins; throwsAggregateErrorif all fail.
async/await is Promise syntax sugar that improves readability.
async function fetchJson(url, options = {}) {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
async function loadProduct(productId) {
try {
const product = await fetchJson(`https://fakestoreapi.com/products/${productId}`);
console.log(product.title);
} catch (error) {
console.error("Could not load product:", error.message);
}
}async function loadSequential() {
const a = await fetchJson("/api/a");
const b = await fetchJson("/api/b");
return { a, b };
}
async function loadParallel() {
const [a, b] = await Promise.all([
fetchJson("/api/a"),
fetchJson("/api/b")
]);
return { a, b };
}Use sequential only when b depends on a.
async function fetchUsers(ids) {
const results = await Promise.allSettled(
ids.map((id) => fetchJson(`/api/users/${id}`))
);
return {
success: results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value),
failed: results
.filter((r) => r.status === "rejected")
.map((r) => r.reason.message)
};
}async function mapWithLimit(items, limit, worker) {
const queue = [...items];
const output = [];
async function runWorker() {
while (queue.length) {
const item = queue.shift();
output.push(await worker(item));
}
}
await Promise.all(Array.from({ length: limit }, runWorker));
return output;
}Use this for large batches (emails, imports, media operations).
async function* pages() {
for (let page = 1; page <= 3; page += 1) {
const data = await fetchJson(`/api/items?page=${page}`);
yield data.items;
}
}
async function consume() {
for await (const items of pages()) {
console.log("received page with", items.length, "items");
}
}let currentController;
async function searchProducts(query) {
if (currentController) currentController.abort();
currentController = new AbortController();
try {
const data = await fetchJson(`/api/search?q=${encodeURIComponent(query)}`, {
signal: currentController.signal
});
renderResults(data);
} catch (error) {
if (error.name === "AbortError") return; // expected during rapid typing
showError(error.message);
}
}async function fetchWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);
try {
return await fetchJson(url, { signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}async function retry(fn, { retries = 3, baseDelay = 300 } = {}) {
let attempt = 0;
while (true) {
try {
return await fn();
} catch (error) {
attempt += 1;
if (attempt > retries) throw error;
const delay = baseDelay * 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}Retry transient failures (timeouts, 502/503/504), not validation errors.
async function loadTodos() {
setViewState("loading");
try {
const todos = await fetchJson("/api/todos");
if (todos.length === 0) {
setViewState("empty");
return;
}
renderTodos(todos);
setViewState("ready");
} catch (error) {
setErrorMessage(error.message);
setViewState("error");
}
}let isSaving = false;
async function onSubmit(formData) {
if (isSaving) return;
isSaving = true;
disableSubmitButton(true);
try {
await fetchJson("/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData)
});
showToast("Saved");
} catch (error) {
showError(error.message);
} finally {
isSaving = false;
disableSubmitButton(false);
}
}- Forgetting
awaitYou pass a Promise where plain data was expected. - Missing
returnin Promise chains Breaks chaining and hides failures. - Using
Array.forEachwithasynccallbackforEachdoes not await. Usefor...oforPromise.all(map(...)). - Not checking
response.okfetchdoes not reject on HTTP 404/500 by default. - Unhandled Promise rejections
Always terminate async flows with
catchortry/catch. - Race conditions in UI Old request finishes after new request and overwrites fresh state.
Use timestamped logs to observe async ordering:
function logStep(step) {
console.log(`${new Date().toISOString()} :: ${step}`);
}- Refactor nested callbacks into
async/await. - Build a "search as you type" input using
AbortController. - Fetch 10 endpoints with concurrency limit
3. - Implement
fetchWithTimeout+ retry wrapper. - Add loading/error/empty states to an API-driven list.
- Every network call checks
response.ok. - Timeouts are defined for user-facing requests.
- Retries only for transient failures.
- Parallel requests used where dependencies allow.
- Aborted/stale requests do not update UI.
- Loading/error/empty states exist for all async screens.
- All Promise chains terminate with error handling.
- JavaScript Fetch API Guide - HTTP requests, response parsing, and API patterns
- DOM Manipulation Guide - Rendering async data safely in the UI
- Frontend Fundamentals Workbook - Integrated JS, DOM, and CSS practice track
- JavaScript LocalStorage Guide - Persisting async results and caching patterns
- JavaScript Basics Cheat Sheet - Core language fundamentals