Async & concurrency
Status: đ© COMPLETE Last updated: 2026-06-19 Plain-English tagline: How code can âwait for something slowâ (a database query, an API call, a file read) without freezing the whole program. The mechanism behind every
await,Promise,then(), andsetTimeoutyouâll ever see in JavaScript.
In plain English
A computer can do MANY things, but one CPU core can only do ONE thing at any single moment. Yet your laptop runs a browser, a music player, a video call, and a code editor all âat the same time.â How?
Two answers:
- Multiple cores â your laptop has 8+ CPU cores, each independently doing work
- Switching very fast â even with one core, the OS switches between tasks thousands of times per second, giving the ILLUSION of simultaneity
For JavaScript specifically, thereâs a third answer that matters more: asynchrony. JavaScript runs on a SINGLE THREAD (one core, one task at a time). It canât be âdoing many things at onceâ within the language. But it CAN say âstart this slow operation, and when itâs done, come back to me.â
Thatâs what async is. When your code says:
const data = await fetch("/api/posts");âŠJavaScript:
- Initiates the network request
- Doesnât wait there. Lets the network do its thing
- Goes off and does other work (handles UI events, runs timers, etc.)
- When the network response arrives, JavaScript comes back and picks up where it left off
datanow has the response; the next line runs
The keywords async, await, Promise, then(), and setTimeout are all about THIS pattern. Theyâre how single-threaded JavaScript handles slow operations without freezing.
This entry is the CS-side view of why async exists and whatâs happening underneath. For specific JS syntax, see JavaScript. For Node-specific event loop details, see Node.js (concept).
Why it matters
Three reasons even a non-coder benefits from understanding async:
-
Almost every line of modern JS is async. Loading data, sending data, reading files, calling APIs, running database queries â all async. Reading code or AI-generated code requires knowing what
awaitactually means. -
The most common bug pattern in modern code is mishandled async. Forgetting
await, mixingthen()andawait, race conditions, unhandled promise rejections â these arenât language quirks, theyâre consequences of single-threaded async. -
Performance discussions hinge on it. âWhy is this slow?â often comes down to âthis is doing one slow thing at a time when it could be doing them in parallelâ â a fundamentally async insight.
The trade-off: async has a learning curve. Understanding the mental model takes effort. After that, modern code reads naturally.
The two big ideas
Idea 1: Concurrency vs. parallelism
These get mixed up constantly. The clean distinction:
- Concurrency â managing MANY tasks IN PROGRESS, even if only one runs at a moment. Like a chef juggling 5 dishes â chopping while pasta boils, stirring while bread rises. Single brain, many tasks âactive.â
- Parallelism â actually DOING multiple things at the SAME instant. Like 5 chefs, each cooking one dish. Multiple brains.
JavaScript is heavily CONCURRENT (one thread juggling many in-progress operations) but rarely PARALLEL (would need Web Workers or worker threads â separate JS contexts).
Idea 2: Blocking vs. non-blocking
When code is âwaitingâ for something:
- Blocking â the whole program stops while waiting. CPU idle. Nothing else can happen.
- Non-blocking â the program registers âwake me when this finishesâ and goes off to do other work. CPU keeps busy.
JavaScriptâs I/O (network, files, timers) is fundamentally non-blocking. Heavy CPU work (calculations, JSON parsing of huge data) is blocking â it monopolizes the thread until done.
The event loop â JavaScriptâs secret
The mechanism behind JSâs async is called the event loop. The model:
1. JavaScript runs your code top-to-bottom.
2. When it hits an async operation (fetch, setTimeout, file read):
- It starts the operation
- Hands off the slow work to the runtime (browser or Node.js)
- Continues running other code
3. When the slow work finishes, the runtime puts the result + callback into a QUEUE.
4. When JavaScript finishes the current synchronous code, the event loop:
- Picks the next callback from the queue
- Runs it
- Repeats
This loop runs continuously. The single thread is never idle when thereâs work to do; it just doesnât BLOCK while waiting for slow operations.
Mental picture:
Main thread: [code] [callback] [callback] [code] [callback] ...
â
picked from queue when main was free
Queue: [fetch result] [timer fired] [file read done]
â
runtime added these
Some operations have priority (microtasks like Promise callbacks always run before macrotasks like setTimeout) but thatâs the broad picture.
The three async syntaxes in JavaScript (in order of evolution)
1. Callbacks (oldest, still around)
Pass a function to be called when the operation completes:
fetchUser(123, (err, user) => {
if (err) return console.error(err);
console.log(user);
});The problem: âcallback hellâ when chaining many async operations.
fetchUser(123, (err, user) => {
fetchPosts(user.id, (err, posts) => {
fetchComments(posts[0].id, (err, comments) => {
// â nested forever
});
});
});2. Promises (mid-2010s)
A Promise is an object representing a future value:
fetchUser(123)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => console.log(comments))
.catch(err => console.error(err));Flatter than callbacks but still chains forever for complex flows.
3. async/await (current standard)
Built on Promises but reads like synchronous code:
try {
const user = await fetchUser(123);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
console.log(comments);
} catch (err) {
console.error(err);
}This is what modern code looks like. The await keyword pauses execution of THIS function until the promise resolves; the rest of the program keeps running.
Parallel vs. sequential awaits
A common mistake: sequential when parallel would work.
Sequential (slow):
const user = await fetchUser(id); // ~100ms
const posts = await fetchPosts(id); // ~100ms (doesn't depend on user)
const comments = await fetchComments(id); // ~100ms (doesn't depend on posts)
// Total: ~300msEach await blocks the function until done.
Parallel (fast):
const [user, posts, comments] = await Promise.all([
fetchUser(id),
fetchPosts(id),
fetchComments(id),
]);
// All three start immediately. Total: ~100ms (the slowest)Promise.all waits for ALL promises to resolve before continuing. If any rejects, the whole thing rejects.
Use sequential when one operation needs the OUTPUT of another. Use parallel when theyâre independent.
A concrete example
Say you want to display a userâs posts AND their friends list:
// Slow â sequential
async function loadUserDashboard(userId) {
const posts = await fetch(`/api/users/${userId}/posts`).then(r => r.json());
const friends = await fetch(`/api/users/${userId}/friends`).then(r => r.json());
return { posts, friends };
}
// Fast â parallel
async function loadUserDashboard(userId) {
const [posts, friends] = await Promise.all([
fetch(`/api/users/${userId}/posts`).then(r => r.json()),
fetch(`/api/users/${userId}/friends`).then(r => r.json()),
]);
return { posts, friends };
}If each fetch takes 200ms, the first version takes 400ms total. The second takes 200ms. Same code, twice as fast.
This is the #1 performance optimization in modern frontend/backend code.
When async goes wrong
A few classic failure modes:
Forgetting await
async function saveUser(user) {
db.users.create(user); // â forgot await
return { ok: true }; // returns before save finishes
}The function returns success before the save completes. If the save fails later, youâve already told the caller it succeeded. The user sees âsavedâ but the data isnât actually saved.
Unhandled promise rejection
fetchUser(123)
.then(user => console.log(user));
// â no .catch(); if fetch fails, the error vanishes silentlyModern Node.js terminates the process on unhandled rejections (good); browsers log a console error (better than nothing). Always .catch() or wrap in try/catch.
Race conditions
async function refreshUserData() {
const user = await fetch(`/api/me`);
setUser(user); // â what if refreshUserData() got called twice in quick succession?
}If the user clicks ârefreshâ twice and the SECOND request finishes FIRST, the older response overwrites the newer. Solution: track a ârequest IDâ and ignore stale responses.
Sequential when parallel works
The example above. Most common in code that grew organically without considering parallelism.
Generators and async iterators (advanced)
For truly stream-like operations (LLM token-by-token output, paginated APIs):
async function* paginatedUsers() {
let page = 1;
while (true) {
const res = await fetch(`/api/users?page=${page}`);
const data = await res.json();
if (data.length === 0) return;
for (const user of data) yield user;
page++;
}
}
for await (const user of paginatedUsers()) {
console.log(user.name);
}The async function* returns an async iterator; for await...of consumes it. Lazy â only fetches the next page when consumed. Very useful for streaming.
Web Workers â actual parallelism in JavaScript
For CPU-heavy work that DOES need to run in parallel, JavaScript has Web Workers (browser) and Worker Threads (Node). Separate JS contexts, separate threads, communicate via message passing.
// main.js
const worker = new Worker("./heavy-task.js");
worker.postMessage({ input: ... });
worker.onmessage = (e) => console.log("Result:", e.data);Workers canât access the DOM directly; theyâre for CPU work, not UI. Used for image processing, big calculations, cryptography, ML inference in the browser.
For typical webapp work, you almost never need workers â Node.js async on the server + Reactâs reconciler in the browser handle everything youâd want.
Common gotchas
-
awaitonly works insideasyncfunctions. Top-level await is a recent addition (Node 14.8+, ES modules). In CommonJS or older code, you need to wrap in anasync functionand call it. -
async functionalways returns a Promise. Even if the function body returns42, the caller getsPromise.resolve(42). To use the value,awaitit. -
Forgetting
awaitis silently broken. TypeScript can warn (@typescript-eslint/no-floating-promises). Without that rule, a missing await results in code that âworksâ inconsistently. -
forEachdoesnât await.array.forEach(async x => await doThing(x))doesnât wait for anything. Usefor...of+await, orPromise.all(array.map(...)). -
mapreturns a Promise array.array.map(async x => x * 2)returnsPromise<number>[]. Needawait Promise.all(...)to unwrap. -
Promise.allrejects if ANY promise rejects. If you want all results regardless of failures, usePromise.allSettledâ returns an array of{status, value}or{status, reason}objects. -
The microtask queue runs before the macrotask queue.
Promise.resolve().then(...)runs BEFOREsetTimeout(..., 0). Surprising; rarely matters but can in tight loops. -
setTimeout(fn, 0)doesnât run immediately. It runs after the current code finishes (and after pending microtasks). The â0â is a minimum, not exact. -
Async functions still run synchronous code synchronously.
async function foo() { console.log("a"); console.log("b"); }logs âaâ then âbâ immediately. Onlyawaitcauses a pause. -
Throwing inside async is fine. Catch with try/catch or
.catch(). Like a sync throw at the surface, just delivered through the promise. -
Resource leaks from forgotten await. A connection opened, work started, then the function returns. The work continues invisibly, with no one to handle errors. Always await async work you initiate.
-
Long synchronous loops block the event loop. A
forloop doing 1M iterations stops EVERYTHING â UI freezes, other callbacks queue up. Break into chunks, use Web Workers, or yield withawait new Promise(r => setTimeout(r, 0)). -
Promise.racereturns the FIRST to settle. Useful for timeouts (Promise.race([fetch(), timeout(5000)])). Other promises continue but their results are ignored. -
async/awaitis sugar over.then(). Same underlying mechanism. Mixing them in one codebase is legal; usually unnecessary. Pick one style. -
Cancellation isnât built into Promises. Once started, you canât âcancelâ a Promise. Use
AbortController+AbortSignalfor cancellable fetches and operations. -
fetchwithout a timeout can hang forever. Combine withAbortSignal.timeout(ms)(modern) orPromise.racewith a timeout promise. -
Streams and
async iteratorsare different. NodeReadablestreams arenât directlyfor await-able without a small adapter (or useReadable.from(...)). -
Reactâs
useEffectdoesnât accept async functions directly.useEffect(async () => {})returns a Promise, not a cleanup function. Wrap:useEffect(() => { const run = async () => {}; run(); }, []). -
Promise.allSettledis the safer Promise.all. When some failures are expected and shouldnât kill the whole batch, allSettled gives you per-item status. -
for await...ofconsumes streams sequentially. If you need parallel processing of a stream, youâd use a worker pool, notfor await. -
Multiple
awaits in sequence on independent operations is almost always a bug. Code review red flag. -
Race conditions are notoriously hard to debug. They manifest as âsometimes it works.â Often: the same user-facing action triggered twice, with stale data overwriting fresh.
-
AI-generated code often does sequential awaits when parallel works. Watch for this; rewrite with
Promise.allwhen independent operations are awaited in sequence. -
Async error handling is its own skill. Errors in async code have stack traces that may not include the calling code. Modern Node + DevTools have improved this, but a thrown error inside a forgotten
thenmay âdisappear.â -
JavaScriptâs single thread is sometimes a benefit. No shared mutable state, no locks, no race conditions WITHIN a function. The downsides (heavy CPU work blocks) are usually solvable.
When async matters most
- API endpoints / server functions â every request handler is async (database, external APIs)
- React components fetching data â
useEffectwith fetch, server components withawait - LLM responses â streaming tokens as they arrive, async iterators
- Background jobs â queues, retries, delayed execution
- WebSocket handlers â events arriving asynchronously
- Form submissions â uploading files, waiting for confirmation
In practice, youâll hit async on every meaningful piece of code. Understanding the mental model pays back constantly.
See also
- JavaScript đ© â Promises + async/await syntax
- TypeScript đ© â typing async functions
- Node.js (concept) đ© đŠ â event loop deep dive
- Data structures â overview đ© â Promises ARE a data structure
- Algorithms â intro đ©
- Time & space complexity (Big-O) đ©
- Recursion đ©
- REST APIs đ© â async by nature
- WebSockets đ© â heavily async
- Webhooks đ© â async-arriving events
- Glossary: async, Promise, await, callback, event loop
Sources
- MDN â Concurrency model and the event loop
- MDN â Using promises
- Node.js â The Event Loop
- JavaScript.info â Promises, async/await
- Jake Archibald â In the Loop (talk) â the canonical event loop explanation
- Philip Roberts â What the heck is the event loop anyway? â the OTHER canonical talk