The short answer
Quick answer: async/await lets you write code that waits for slow operations, such as network requests, without blocking a thread. An async function returns a promise (also called a future or task) immediately. When execution reaches await, the function pauses, saves its local state, and returns control to an event loop, which runs other work in the meantime. When the awaited operation finishes, the event loop resumes the function right where it left off. Under the hood, the compiler turns each async function into a state machine.
The problem being solved
Most real programs spend much of their time waiting: for a database, a disk, another server. There are two classic ways to handle that.
Block a thread. The simplest code calls a function and waits for the answer. But a waiting thread still costs memory and scheduling overhead. A server holding ten thousand slow connections would need ten thousand threads. See processes vs threads.
Use callbacks. Start the operation, pass a function to call when it is done, and carry on. This is efficient, but the logic turns inside out:
getUser(id, (err, user) => {
if (err) return handle(err);
getOrders(user, (err, orders) => {
if (err) return handle(err);
getItems(orders[0], (err, items) => {
// and so on, deeper and deeper
});
});
});
Error handling is repeated at every level, and loops and early returns become awkward. This shape was nicknamed "callback hell".
Promises: a value that arrives later
A promise is an object standing in for a result that is not ready yet. It is in one of three states: pending, fulfilled with a value, or rejected with an error. You attach reactions with .then() and .catch(). MDN's guide to using promises covers the details.
Promises flatten the nesting and let errors flow to one handler:
getUser(id)
.then(user => getOrders(user))
.then(orders => getItems(orders[0]))
.catch(handle);
Better, but still not how people naturally write step-by-step logic.
Async/await: promises with normal syntax
async/await is a thin layer on top of promises:
async function loadItems(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user);
return await getItems(orders[0]);
} catch (err) {
handle(err);
}
}
Two rules explain everything:
- An
asyncfunction always returns a promise. Returning a value fulfils it; throwing rejects it. awaitpauses only this function until the awaited promise settles. It does not block the thread. Other code keeps running.
Ordinary try/catch, loops and if statements all work as usual.
What the compiler really does
A normal function cannot stop in the middle and continue later, because its local variables live in a stack frame that is destroyed when it returns. So the compiler or runtime rewrites every async function into a state machine:
- Local variables move into an object that survives between steps.
- The body is split at each
awaitinto numbered states. - A "resume" method runs from the current state to the next
await, registers itself as the continuation, and returns.
Conceptually, the function above becomes something like this:
function loadItems(id) {
let state = 0, user, orders;
return new Promise((resolve, reject) => {
function step(value) {
try {
switch (state) {
case 0: state = 1; return getUser(id).then(step, reject);
case 1: user = value; state = 2; return getOrders(user).then(step, reject);
case 2: orders = value; state = 3; return getItems(orders[0]).then(step, reject);
case 3: return resolve(value);
}
} catch (e) { reject(e); }
}
step();
});
}
C# and Rust generate state machines at compile time. JavaScript and Python build on generators or coroutines, functions with a built-in ability to suspend and resume. The idea is the same in all of them.
The event loop ties it together
Something has to resume paused functions. That is the event loop: a loop that waits for events (a timer fired, a socket has data, a file read finished) and runs the code waiting on them. The operating system reports ready I/O through efficient notification system calls such as epoll.
The flow for one await:
- The function starts an I/O operation and gets a pending promise.
awaitsuspends the function and returns to the event loop.- The loop runs other tasks.
- The operating system signals that the I/O finished.
- The loop resolves the promise and schedules the function's continuation.
- The function resumes with the result.
For a deeper look at the loop itself, see what is the event loop.
Concurrency is not parallelism
Async code on a single thread is concurrent: many operations are in progress at once. It is not parallel: only one piece of your code runs at any instant. That is ideal for I/O-heavy work, and useless for CPU-heavy work. A long calculation in an async function blocks the event loop and freezes everything else.
To run several operations at the same time, start them all before awaiting:
// Sequential: total time is the sum
const a = await fetchA();
const b = await fetchB();
// Concurrent: total time is the longest one
const [a, b] = await Promise.all([fetchA(), fetchB()]);
Python's equivalent is asyncio.gather, described in the asyncio documentation.
Common mistakes
- Forgetting
await. You get a promise object instead of a value, and errors go unhandled. - Awaiting in a loop when the operations are independent. Use
Promise.allorgather. - Blocking the loop with heavy computation or synchronous I/O. Move it to a worker thread or process.
- Unhandled rejections. Every promise needs an error handler somewhere up the chain.
- "Async contagion". Once a function is async, its callers must be async too. Plan the boundary between sync and async code.
- No timeout or cancellation. An await can wait forever unless you add one.
Frequently asked questions
Does await block the thread?
No. It suspends the current function and frees the thread to do other work.
Is async code faster?
Not for a single operation. It lets one thread handle many waiting operations at once, which improves throughput and uses less memory than a thread per task.
When should I not use async/await?
For CPU-bound work. Async helps with waiting, not with computing. Use threads, processes or workers for heavy calculation.
What is the difference between a promise, a future and a task?
They are different names for the same concept: an object representing a result that will be available later.
Conclusion
Async/await is syntax, not magic. The compiler turns your function into a resumable state machine, promises carry results and errors between steps, and an event loop decides what runs next. Once you see that await means "pause here and let others run", most async puzzles become easy to reason about.
Related articles
- What Is the Event Loop and Why Is Node.js Single-Threaded?
- Processes vs Threads: What's the Real Difference?
- What Is a System Call and Why Is It Expensive?
- How WebSockets Enable Real-Time Apps Like Chat and Live Scores
