8.0 The Problem: Blocking Code
JavaScript runs on a single thread. A long synchronous operation — reading a
file, querying a database, waiting for a network response — blocks every other
operation until it finishes. The page freezes; the user sees no response to clicks or
key presses.
// bad: synchronous network (hypothetical — blocks the thread)
const data = fetchSync('/api/items'); // nothing else can run here
render(data);
JavaScript solves this with asynchronous patterns that let the engine
continue processing other events while waiting for a slow operation to complete.
Three patterns exist, in historical order: callbacks, Promises, and
async/await.
8.1 Callbacks
The original async pattern: pass a function to be called when the operation completes.
setTimeout(() => {
console.log('ran after 1 second');
}, 1000);
// Node-style: error-first callback
fs.readFile('data.txt', 'utf8', (err, text) => {
if (err) { console.error(err); return; }
console.log(text);
});
Callbacks work, but nesting them for sequential async steps produces deeply indented,
hard-to-follow code known as callback hell:
getUser(id, (err, user) => {
if (err) return handleErr(err);
getOrders(user.id, (err, orders) => {
if (err) return handleErr(err);
getItems(orders[0].id, (err, items) => {
if (err) return handleErr(err);
render(items); // three levels deep, and errors repeat
});
});
});
8.2 Promises
A Promise is an object representing the eventual result of an async
operation. It is always in one of three states:
- pending — the operation has not completed yet.
- fulfilled — the operation succeeded; the Promise holds its result value.
- rejected — the operation failed; the Promise holds a reason (usually an Error).
8.2.1 Creating a Promise
function delay(ms) {
return new Promise((resolve, reject) => {
if (ms < 0) { reject(new Error('negative delay')); return; }
setTimeout(() => resolve(ms), ms);
});
}
delay(500).then(ms => console.log(`done after ${ms}ms`));
The constructor receives an executor function with two callbacks:
call resolve(value) on success, reject(reason) on failure.
8.2.2 Chaining
.then(onFulfilled) returns a new Promise, enabling flat sequential chains
that replace nested callbacks:
getUser(id)
.then(user => getOrders(user.id))
.then(orders => getItems(orders[0].id))
.then(items => render(items))
.catch(err => handleErr(err)); // one error handler for the whole chain
.catch(fn) is shorthand for .then(undefined, fn). A
.finally(fn) block runs regardless of outcome and is useful for cleanup.
8.2.3 Promise Combinators
Promise.all, race, allSettled, any
| Combinator |
Behavior |
Promise.all(iterable) |
Fulfills when all fulfill; rejects as soon as any one rejects. Result is an array of values in input order. |
Promise.allSettled(iterable) |
Always fulfills when every Promise settles. Result is an array of {status, value|reason} objects — no early rejection. |
Promise.race(iterable) |
Settles with the first Promise to settle, fulfilled or rejected. |
Promise.any(iterable) |
Fulfills with the first fulfilled value. Rejects with AggregateError only if all reject. |
Promise.resolve(value) |
Returns a Promise already fulfilled with value. |
Promise.reject(reason) |
Returns a Promise already rejected with reason. |
// fetch three resources in parallel, wait for all
const [users, posts, tags] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/tags').then(r => r.json()),
]);
8.3 async / await
async/await is syntactic sugar over Promises that lets you
write async code with the same linear structure as synchronous code.
-
Marking a function async makes it always return a Promise.
-
await expr pauses the function (not the thread) until the
Promise resolves, then evaluates to the fulfilled value.
-
await may only appear inside an async function or at the
top level of a module.
async function loadDashboard(userId) {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const items = await getItems(orders[0].id);
render(items);
}
This is exactly equivalent to the .then chain above, but reads top-to-bottom
like synchronous code.
8.4 Error Handling
Use try/catch inside async functions instead of
.catch() on a chain:
async function loadDashboard(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
render(orders);
} catch (err) {
showError(err.message);
} finally {
hideSpinner();
}
}
Always handle rejections. An unhandled Promise rejection triggers the browser's
unhandledrejection event and in Node logs a warning or crashes the process:
window.addEventListener('unhandledrejection', e => {
console.error('Unhandled rejection:', e.reason);
});
8.5 The Fetch API
fetch(url, options) is the modern way to make HTTP requests from the browser.
It returns a Promise for a Response object.
async function getItems() {
const resp = await fetch('/api/items');
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json(); // .json() also returns a Promise
}
Key detail: fetch only rejects on network failure, not on HTTP error
status codes like 404 or 500. Always check resp.ok (or
resp.status) explicitly.
8.5.1 Fetch Options
const resp = await fetch('/api/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'widget' }),
signal: controller.signal, // AbortController for cancellation
});
Common fetch options
| Option |
Purpose |
method | HTTP verb: GET (default), POST, PUT, PATCH, DELETE. |
headers | Object or Headers instance of request headers. |
body | Request body: string, FormData, Blob, ReadableStream, etc. Not allowed on GET/HEAD. |
credentials | omit | same-origin (default) | include — whether to send cookies cross-origin. |
mode | cors (default) | no-cors | same-origin — CORS policy for the request. |
cache | default | no-store | reload | force-cache | only-if-cached. |
signal | AbortSignal from an AbortController to cancel the request. |
8.5.2 Reading the Response
Response methods and properties
| Member |
Purpose |
resp.ok | true when status is 200–299. |
resp.status | HTTP status code (200, 404, 500, …). |
resp.headers | Headers object; use .get('content-type') to read. |
resp.json() | Parse body as JSON; returns a Promise. |
resp.text() | Read body as a UTF-8 string; returns a Promise. |
resp.formData() | Parse body as FormData; returns a Promise. |
resp.blob() | Read body as a Blob; returns a Promise. |
resp.arrayBuffer() | Read body as an ArrayBuffer; returns a Promise. |
resp.body | ReadableStream for streaming large responses. |
8.5.3 Cancelling a Request
const controller = new AbortController();
// cancel after 5 seconds
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const resp = await fetch('/api/slow', { signal: controller.signal });
const data = await resp.json();
render(data);
} catch (err) {
if (err.name === 'AbortError') {
console.log('request was cancelled');
} else {
throw err;
}
} finally {
clearTimeout(timeout);
}
8.6 Common Async Patterns
Async pattern recipes
| Pattern |
Code |
| Sequential steps |
const a = await step1();
const b = await step2(a);
const c = await step3(b);
|
| Parallel requests |
const [a, b] = await Promise.all([
fetchA(), fetchB()
]);
|
| Retry on failure |
async function withRetry(fn, n = 3) {
for (let i = 0; i < n; i++) {
try { return await fn(); }
catch (e) { if (i === n-1) throw e; }
}
}
|
| Debounce async |
let timer;
input.addEventListener('input', () => {
clearTimeout(timer);
timer = setTimeout(() => search(input.value), 300);
});
|
| Sequential array |
for (const id of ids) {
await processOne(id); // one at a time
}
|
| Parallel array |
await Promise.all(
ids.map(id => processOne(id)) // all at once
);
|
8.7 Tutorials
Async JavaScript Tutorials
| Tutorial |
Type |
Why Use It |
| MDN: Asynchronous JavaScript |
Beginner→Intermediate |
Complete series from callbacks through async/await with examples. |
| javascript.info: Async/await |
Intermediate |
Clear, concise explanations with runnable examples for each concept. |
| MDN: Using the Fetch API |
Intermediate |
Comprehensive guide to fetch options, response handling, and error patterns. |
| javascript.info: Promises |
Beginner→Intermediate |
Four-part series covering creation, chaining, error handling, and combinators. |
| web.dev: JavaScript Promises |
Intermediate |
Thorough treatment of promise states, chaining, and parallelism. |
| Jake Archibald: Event Loop (talk) |
Deep dive |
Visual explanation of how the event loop, tasks, and microtasks interact. |
8.8 References
Async JavaScript References
| Resource |
Description |
| MDN: Promise |
Full API reference for Promise, including all static methods and instance methods. |
| MDN: async function |
Reference for async function syntax, return values, and interaction with await. |
| MDN: Fetch API |
Complete reference for fetch(), Request, Response, and Headers. |
| MDN: AbortController |
API for cancelling fetch requests and other async operations. |
| ECMAScript spec: Promises |
Formal specification of the Promise abstract operations and state machine. |
| WHATWG: Event Loops |
Authoritative specification of the event loop, task queues, and microtask checkpoint. |